diff --git a/docs/guides/javascript/v8-code-cache.md b/docs/guides/javascript/v8-code-cache.md index 464c6c70ef..40c2c69ce7 100644 --- a/docs/guides/javascript/v8-code-cache.md +++ b/docs/guides/javascript/v8-code-cache.md @@ -54,7 +54,8 @@ reader reports it under the name the measurement supports. Treat that name as an observation, not as a claim about what any other build stores there. `header.producer` names the producer family from the flag bits in that word. Bit 29 marks `vm.compileFunction` (`compile_function`); bit 30 marks `vm.SourceTextModule` -(`module`). When neither flag is present, the producer remains `unknown`: +(`module`). V8 before 13.4, including Node 22, marks a module with bit 31 instead, +which is also reported as `module`. When no flag is present, the producer remains `unknown`: `vm.Script`, bytenode output and older unflagged function caches cannot be separated by those bits alone. The source-length mask is `0x1fffffff`, so both flags sit outside the reported length. This describes the admitted header diff --git a/src/domain/v8/v8CodeCache.ts b/src/domain/v8/v8CodeCache.ts index 666e53ca92..cf52f26231 100644 --- a/src/domain/v8/v8CodeCache.ts +++ b/src/domain/v8/v8CodeCache.ts @@ -40,6 +40,15 @@ export const V8_CODE_CACHE_COMPILE_FUNCTION_BIT = 0x20000000; * `0x1fffffff`, so this bit and the function bit are both outside it. */ export const V8_CODE_CACHE_MODULE_BIT = 0x40000000; +/** + * Bit 31 of the source-length word marks a module cache written by V8 before 13.4. + * + * Those builds compute the word as `source_length | kModuleFlagMask` with + * `kModuleFlagMask = 1 << 31` (`SerializedCodeData::SourceHash`); 13.4 moved the flag to + * bit 30. Measured on Electron 30.1.2 (V8 12.4.254.20): a 23-unit module source reads + * `0x80000017`. Node 22 runs V8 12.4, so its module caches carry this bit. + */ +export const V8_CODE_CACHE_LEGACY_MODULE_BIT = 0x80000000; /** Source length occupies the low bits of that word. */ export const V8_CODE_CACHE_SOURCE_LENGTH_MASK = 0x1fffffff; diff --git a/src/v8/V8CodeCacheParser.test.ts b/src/v8/V8CodeCacheParser.test.ts index f1ec862b06..3fa88ec0a6 100644 --- a/src/v8/V8CodeCacheParser.test.ts +++ b/src/v8/V8CodeCacheParser.test.ts @@ -118,6 +118,9 @@ it("keeps the function and module flags distinct", () => { expect( parse(withHeaderWord(script, 2, 0x40000000 | length)).header.producer, ).toBe("module"); + // V8 before 13.4 (Node 22's 12.4) marks a module with bit 31: `0x80000017` on Electron 30. + const legacy = parse(withHeaderWord(script, 2, 0x80000000 | length)).header; + expect([legacy.producer, legacy.source_length]).toEqual(["module", length]); // Both bits set is not a shape V8 produces; the module flag wins so the reported // family stays one of the three observed kinds. expect( diff --git a/src/v8/V8CodeCacheParser.ts b/src/v8/V8CodeCacheParser.ts index 8a69024590..4616a92c8d 100644 --- a/src/v8/V8CodeCacheParser.ts +++ b/src/v8/V8CodeCacheParser.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { MAX_INSPECT_V8_CODE_CACHE_STRING_BYTES, V8_CODE_CACHE_COMPILE_FUNCTION_BIT, + V8_CODE_CACHE_LEGACY_MODULE_BIT, V8_CODE_CACHE_MODULE_BIT, V8_CODE_CACHE_HEADER_BYTES, V8_CODE_CACHE_MAGIC_HIGH, @@ -48,6 +49,9 @@ const LIMITATIONS = [ "An unflagged producer word is reported as unknown: whether V8 marks a vm.compileFunction cache at all is a build detail, so the container cannot separate a vm.Script from an unmarked function cache on its own.", ]; +/** Bit 30 (V8 13.4+) or bit 31 (earlier V8, including Node 22) marks a module cache. */ +const MODULE_BITS = V8_CODE_CACHE_MODULE_BIT | V8_CODE_CACHE_LEGACY_MODULE_BIT; + /** Printable ASCII and horizontal tab, the run alphabet of one observation. */ const isPrintable = (byte: number): boolean => (byte >= 0x20 && byte <= 0x7e) || byte === 0x09; @@ -105,7 +109,7 @@ export const parseV8CodeCache = ( // separates them. `fields` keeps the observed word so a caller can resolve it against a // build fingerprint it trusts. const producer = - (sourceWord & V8_CODE_CACHE_MODULE_BIT) !== 0 + (sourceWord & MODULE_BITS) !== 0 ? ("module" as const) : (sourceWord & V8_CODE_CACHE_COMPILE_FUNCTION_BIT) !== 0 ? ("compile_function" as const)