Files
reverse-engineering-anything/tests/boundary/filesystem/javascript-applications/electronVmCachedData.test.ts
T
2c27c2a43b feat(electron): report V8 cachedData compilation boundaries (#9501)
* feat(electron): report V8 cachedData execution boundaries

analyze-javascript-application treated new vm.Script(source, {cachedData}) and vm.compileFunction(..., {cachedData}) as ordinary source. V8 deserializes previously compiled bytecode for those calls, so an Electron preload wrapper that delegates its whole contextBridge and IPC surface to a .cache/.jsc snapshot appeared to have been fully analyzed with zero exposed APIs.

Record each vm compilation with its inert cache path when the cachedData value traces to a readFileSync/readFile of path syntax, and project a cached-data execution unknown plus a loads edge from the owning source. Compilations without a cache stay recorded as no-cached-data so absence remains distinguishable from an unparsed snapshot. Count cachedData executions in the Electron summary and state the coverage limit in the graph limitations.

* style(cli): wrap the native data type path description

The `inspect-native-data-type` argument description line is 82 columns, over the repository's 80-column `.oxfmtrc.json` limit, so `npm run format:check` fails on main and on every branch merged from it. It came in with #9465 and the following commits to this file did not touch it. Reformat the one call the formatter objects to; no behaviour change.

* style(domain): reformat the pending dynamic-code accumulator field

`npm run format:check` fails on this branch, which is what `Static checks` reports: oxfmt wants the `pendingDynamicCode` union on one line. Applying it is the whole change; no behaviour is touched.

* fix(electron): preserve VM callable and cache source uncertainty

* docs(javascript): specify cached-data compilation boundaries

* fix(electron): exclude disabled caches from recovered boundaries

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 20:16:18 +08:00

190 lines
6.7 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import {
analyzeJavaScriptApplication,
reconstructJavaScriptArtifact,
} from "../../../support/javascriptApplicationScope.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js";
const WRAPPER = String.raw`
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const cachedData = fs.readFileSync(path.join(__dirname, "preload.bundle.cache"));
new vm.Script(sourcePlaceholder, { cachedData }).runInThisContext();
`;
const vmFixture = async (source: string): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-vm-cache-");
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({ name: "rea-electron-vm-cache", main: "main.cjs" }),
),
writeFile(join(root, "main.cjs"), source),
writeFile(join(root, "preload.bundle.cache"), Buffer.from([0, 1, 2, 255])),
]);
return root;
};
describe("V8 cached data compilation boundaries", () => {
it("projects an explicit unknown for the bytecode handoff", async () => {
const result = await reconstructJavaScriptArtifact({
input_path: await vmFixture(WRAPPER),
});
const graph = parseJavaScriptApplicationGraph(result.graph);
const unknown = graph.nodes.filter(
({ kind, observations }) =>
kind === "unknown" &&
observations.some(
({ properties }) =>
properties.compilation_boundary === "v8-cached-data",
),
);
expect(unknown).toHaveLength(1);
expect(unknown[0]?.observations[0]?.properties).toMatchObject({
api: "vm.Script",
cached_data_status: "declared",
cache_path: "preload.bundle.cache",
resolved_cache_path: null,
candidate_cache_path: "preload.bundle.cache",
resolution_status: "unavailable",
source_expression: "sourcePlaceholder",
});
expect(unknown[0]?.observations[0]?.evidence.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("does not establish valid cache bytes"),
]),
);
expect(
graph.edges.filter(({ target_node_id: target }) =>
unknown.some(({ node_id }) => node_id === target),
),
).toEqual([
expect.objectContaining({
relation: "loads",
properties: expect.objectContaining({ api: "vm.Script" }),
}),
]);
});
it("stays silent when compilation has no bytecode cache", async () => {
const result = await reconstructJavaScriptArtifact({
input_path: await vmFixture(String.raw`
const vm = require("node:vm");
new vm.Script("require('node:fs')").runInThisContext();
vm.compileFunction("return 1", [], {});
`),
});
const graph = parseJavaScriptApplicationGraph(result.graph);
expect(
graph.nodes.filter(({ observations }) =>
observations.some(
({ properties }) =>
properties.compilation_boundary === "v8-cached-data",
),
),
).toEqual([]);
expect(graph.limitations.some((note) => note.includes("cachedData"))).toBe(
false,
);
});
});
describe("V8 cached data summary", () => {
it("counts cachedData declarations without claiming an empty preload", async () => {
const result = await analyzeJavaScriptApplication({
input_path: await vmFixture(WRAPPER),
});
if (!result.ok) throw result.error;
expect(parseEvidence(result.value)).toMatchObject({
normalized_result: {
summary: expect.objectContaining({ v8_cached_data_compilations: 1 }),
},
});
});
});
it("keeps missing, dynamic and arbitrary cachedData sources distinct", async () => {
for (const [options, status, path] of [
[
'{ cachedData: fs.readFileSync("./missing.cache") }',
"declared",
"./missing.cache",
],
["getOptions()", "unknown", null],
['{ cachedData: "./preload.bundle.cache" }', "declared", null],
] as const) {
const root = await vmFixture(
`const fs = require("node:fs"); const vm = require("node:vm"); new vm.Script("void 0", ${options});`,
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const observation = graph.nodes
.flatMap((node) => node.observations)
.find(
({ properties }) =>
properties.compilation_boundary === "v8-cached-data",
);
if (options.includes('cachedData: "'))
expect(observation?.properties.cache_expression).toBe(
'"./preload.bundle.cache"',
);
expect(observation?.properties).toMatchObject({
cached_data_status: status,
cache_path: path,
resolved_cache_path: null,
});
}
});
it("does not resolve a cwd-relative filesystem cache to an inventoried source sibling", async () => {
const root = await vmFixture(
`const fs = require("node:fs"); const vm = require("node:vm"); new vm.Script(source, { cachedData: fs.readFileSync("./preload.bundle.cache") });`,
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const observation = graph.nodes
.flatMap(({ observations }) => observations)
.find(
({ properties }) => properties.compilation_boundary === "v8-cached-data",
);
expect(observation?.properties).toMatchObject({
cache_path: "./preload.bundle.cache",
cache_path_base: "literal-path",
resolved_cache_path: null,
});
});
it("omits opaque-cache graph boundaries for proven undefined cache bytes", async () => {
const root = await vmFixture(
'const vm = require("node:vm"); new vm.Script(source, { cachedData: void 0 });',
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
expect(
graph.nodes.some(({ observations }) =>
observations.some(
({ properties }) =>
properties.compilation_boundary === "v8-cached-data",
),
),
).toBe(false);
});
it("does not count source-only VM compilation as recovered Electron semantics", async () => {
const root = await vmFixture(
'const electron = require("electron"); const vm = require("node:vm"); new vm.Script(source);',
);
const result = await analyzeJavaScriptApplication({ input_path: root });
if (!result.ok) throw result.error;
expect(parseEvidence(result.value).normalized_result).toMatchObject({
summary: { electron_semantics: "empty", v8_cached_data_compilations: 0 },
});
});