mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
* 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>
190 lines
6.7 KiB
TypeScript
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 },
|
|
});
|
|
});
|