mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
234 lines
9.0 KiB
TypeScript
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>;
|