Files
reverse-engineering-anything/src/domain/v8/v8CodeCache.ts
T

234 lines
9.0 KiB
TypeScript

import { z } from "zod";
import { digestSchema } from "../digests.js";
import { absoluteLocalPathStringSchema } from "../localPath.js";
const uint = z.number().int().min(0).max(0xffffffff);
const offset = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
/**
* Hard ceiling on the caller-selected input read budget. The payload scan is a
* single synchronous pass, so this bounds how long one request can hold the
* process; it matches the pinned CPython pyc reader's 64 MiB ceiling.
*/
export const MAX_INSPECT_V8_CODE_CACHE_FILE_BYTES = 67108864;
/** Hard ceiling on the V8 code cache container header this reader accepts. */
export const V8_CODE_CACHE_HEADER_BYTES = 32;
/**
* V8 stamps every serialized code cache with `0xC0DE0000 ^ ExternalReferenceTable::kSize`.
* Only the high half is stable across builds, so the low half is a build fingerprint and
* is never used as an admission condition.
*/
export const V8_CODE_CACHE_MAGIC_HIGH = 0xc0de;
/** Hard ceiling on reported embedded byte-pattern observations. */
export const MAX_INSPECT_V8_CODE_CACHE_STRINGS = 65536;
/** Shortest printable run reported as a byte-pattern observation. */
export const MIN_INSPECT_V8_CODE_CACHE_STRING_BYTES = 4;
/** Upper bound the input schema admits for `min_string_bytes`. */
export const MAX_INSPECT_V8_CODE_CACHE_MIN_STRING_BYTES = 256;
/** Longest single reported run; longer runs are truncated and said to be truncated. */
export const MAX_INSPECT_V8_CODE_CACHE_STRING_BYTES = 4096;
/** `vm.compileFunction` sets this bit in the source-length word. */
export const V8_CODE_CACHE_COMPILE_FUNCTION_BIT = 0x20000000;
/**
* Bit 30 of the source-length word marks `vm.SourceTextModule` output.
*
* Measured on Node 24.22.1: a module cache reads `0x40000013` for a 19-unit source and a
* script cache reads `0x00000013` for the same length. The source-length mask is
* `0x1fffffff`, so this bit and the function bit are both outside it.
*/
export const V8_CODE_CACHE_MODULE_BIT = 0x40000000;
/** Source length occupies the low bits of that word. */
export const V8_CODE_CACHE_SOURCE_LENGTH_MASK = 0x1fffffff;
/** Bounded inspection of one V8 code cache container. Never executes the payload. */
export const inspectV8CodeCacheInputSchema = z.strictObject({
path: absoluteLocalPathStringSchema.describe(
"Absolute filesystem path to the selected V8 code cache (.jsc) file",
),
max_file_bytes: z
.number()
.int()
.min(36)
.max(MAX_INSPECT_V8_CODE_CACHE_FILE_BYTES)
.default(32 * 1024 * 1024)
.describe(
"Maximum stable artifact bytes to read; exceeding the budget fails without partial success",
),
max_strings: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_V8_CODE_CACHE_STRINGS)
.default(4096)
.describe(
"Maximum embedded byte-pattern observations to report; exceeding the budget fails without partial success",
),
min_string_bytes: z
.number()
.int()
.min(MIN_INSPECT_V8_CODE_CACHE_STRING_BYTES)
.max(MAX_INSPECT_V8_CODE_CACHE_MIN_STRING_BYTES)
.default(MIN_INSPECT_V8_CODE_CACHE_STRING_BYTES)
.describe("Shortest printable run reported as one observation"),
});
export type InspectV8CodeCacheInput = z.infer<
typeof inspectV8CodeCacheInputSchema
>;
/** Every header role is reported as observed, with how confident that role is. */
export const V8_CODE_CACHE_FIELD_STATUSES = [
"confirmed",
"inferred",
"unconfirmed",
] as const;
/**
* One 32-bit header word. `role` names what the reader believes the word is; `status`
* says whether that role is demonstrated, inferred from behaviour, or merely observed.
* The container is version-dependent, so this table is data, not a fixed ABI.
*/
const headerFieldSchema = z.strictObject({
/** Byte offset of this word inside the container header. */
offset: uint,
/** The observed little-endian value. */
value: uint,
role: z.string().min(1),
status: z.enum(V8_CODE_CACHE_FIELD_STATUSES),
});
const headerSchema = z.strictObject({
/** The full observed magic word. Its low half is a V8 build fingerprint. */
magic: uint,
/** High half of the magic, the only part stable across V8 builds. */
magic_build_stable: uint,
/** Every header word, in container order. */
fields: z.array(headerFieldSchema).length(8),
/** Bytes this reader treats as header before the payload begins. */
header_bytes: uint,
/** Source length in UTF-16 code units, as recorded by the producer, if determinable. */
source_length: uint.nullable(),
/**
* Producer family observed from the source-length word.
*
* V8 sets bit 29 for `vm.compileFunction` and bit 30 for `vm.SourceTextModule`, the two
* upper bits the source-length mask leaves out. Measured on Node 24.22.1: a module cache
* reads `0x40000013` for a 19-unit source, so a producer check that only looks at the
* function bit reports a module cache as a plain script.
*
* `unknown` is the unflagged case, and it is not `vm.Script` for certain: whether V8
* marks a function cache at all is a build detail. Measured on Node 24.22.1 a
* `vm.compileFunction` cache reads `0x20000004` for a 4-unit source while Node 22.23.4
* writes `0x00000004` for the same call. An unflagged word is therefore either
* `vm.Script` or a `vm.compileFunction` cache from a build that does not mark it, and
* the file carries nothing that separates the two.
*/
producer: z.enum(["script", "compile_function", "module", "unknown"]),
});
/** One printable run found in the payload. An observation, not a decoded heap object. */
const embeddedStringSchema = z.strictObject({
/** Absolute byte offset in the selected file. */
offset,
bytes: uint,
/** How many bytes were retained; shorter than `bytes` when the run was truncated. */
retained_bytes: uint,
truncated: z.boolean(),
text: z.string(),
});
export const v8CodeCacheSchema = z
.strictObject({
artifact: z.strictObject({
path: absoluteLocalPathStringSchema,
sha256: digestSchema,
bytes: offset,
}),
header: headerSchema,
payload: z.strictObject({
offset,
bytes: offset,
sha256: digestSchema,
/** Header bytes after the container header that are not part of the declared payload. */
trailing_bytes: offset,
}),
/**
* Byte-pattern observations inside the payload. Presence of a literal here is not
* proof V8 retained it as a heap string, and absence is not proof it did not.
*/
embedded_strings: z.array(embeddedStringSchema),
coverage: z.strictObject({
status: z.literal("complete"),
/** Payload bytes examined for printable runs. */
scanned_bytes: offset,
strings: uint,
}),
limitations: z.array(z.string().min(1)).min(1),
})
.superRefine((report, context) => {
if (report.header.header_bytes !== V8_CODE_CACHE_HEADER_BYTES)
context.addIssue({
code: "custom",
path: ["header", "header_bytes"],
message: "This reader only accepts its pinned container header size.",
});
if (report.header.magic_build_stable !== V8_CODE_CACHE_MAGIC_HIGH)
context.addIssue({
code: "custom",
path: ["header", "magic_build_stable"],
message:
"The admitted magic must carry the stable V8 code cache high half.",
});
if (report.payload.offset + report.payload.bytes !== report.artifact.bytes)
context.addIssue({
code: "custom",
path: ["payload", "bytes"],
message:
"The declared payload length must reach the end of the selected file.",
});
if (report.coverage.strings !== report.embedded_strings.length)
context.addIssue({
code: "custom",
path: ["coverage", "strings"],
message:
"Observation coverage disagrees with the listed embedded strings.",
});
if (report.coverage.scanned_bytes !== report.payload.bytes)
context.addIssue({
code: "custom",
path: ["coverage", "scanned_bytes"],
message: "Complete coverage must examine every declared payload byte.",
});
report.embedded_strings.forEach((observation, index) => {
if (observation.retained_bytes > observation.bytes)
context.addIssue({
code: "custom",
path: ["embedded_strings", index, "retained_bytes"],
message: "A retained prefix cannot exceed the observed run length.",
});
if (
observation.truncated !==
observation.retained_bytes < observation.bytes
)
context.addIssue({
code: "custom",
path: ["embedded_strings", index, "truncated"],
message: "Truncation must agree with the retained byte count.",
});
if (
observation.offset < report.payload.offset ||
observation.offset + observation.bytes > report.artifact.bytes
)
context.addIssue({
code: "custom",
path: ["embedded_strings", index, "offset"],
message: "An observation must lie inside the declared payload.",
});
});
});
export type V8CodeCache = z.infer<typeof v8CodeCacheSchema>;