mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
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>
This commit is contained in:
21 files changed
+982
-4
No files matched your search
@@ -0,0 +1,6 @@
|
||||
import type { ElectronDynamicCodeFinding } from "./electronStaticAnalysisTypes.js";
|
||||
|
||||
/** A source-only compilation cannot establish an opaque cache boundary. */
|
||||
export const isPossibleCachedDataCompilation = (
|
||||
finding: ElectronDynamicCodeFinding,
|
||||
): boolean => finding.cached_data_status !== "absent";
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { inspectElectronBrowserNode } from "./electronStaticAnalysisBrowser.js";
|
||||
import { deferElectronDynamicCode } from "./electronStaticAnalysisDynamicCode.js";
|
||||
import { inspectElectronIpcNode } from "./electronStaticAnalysisIpc.js";
|
||||
import { inspectElectronNativeNode } from "./electronStaticAnalysisNative.js";
|
||||
import { inspectElectronProtocolNode } from "./electronStaticAnalysisProtocol.js";
|
||||
@@ -20,6 +21,7 @@ export const inspectElectronStaticNode = (
|
||||
inspectElectronBrowserNode(node, context);
|
||||
inspectElectronIpcNode(node, context);
|
||||
inspectElectronNativeNode(node, context);
|
||||
deferElectronDynamicCode(node, context);
|
||||
const path =
|
||||
t.isCallExpression(node) || t.isOptionalCallExpression(node)
|
||||
? (electronCalleePath(node.callee, context) ??
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
|
||||
|
||||
// A `.js` fixture parses as ESM, and then every `require` call keeps only
|
||||
// candidate loader authority. These cases describe CommonJS packaging, so they
|
||||
// use the CommonJS fixture that the sibling Electron paths also rely on.
|
||||
const analyze = (source: string) => analyzeCommonJsStaticFixture(source);
|
||||
|
||||
describe("node:vm bytecode cache boundaries", () => {
|
||||
it("records the artifact named by an inert readFileSync cache argument", () => {
|
||||
const analysis = analyze(`
|
||||
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();
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code).toEqual([
|
||||
expect.objectContaining({
|
||||
api: "vm.Script",
|
||||
cached_data_status: "declared",
|
||||
cached_data: {
|
||||
source_path: "preload.bundle.cache",
|
||||
source_path_base: "source-directory",
|
||||
source_resolution_context: "filesystem-expression",
|
||||
source_expression: null,
|
||||
},
|
||||
source_text: expect.objectContaining({
|
||||
expression: "sourcePlaceholder",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves compileFunction's positional cachedData argument", () => {
|
||||
const analysis = analyze(`
|
||||
const vm = require("node:vm");
|
||||
vm.compileFunction(text, [], { cachedData: loadCache() });
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code).toEqual([
|
||||
expect.objectContaining({
|
||||
api: "vm.compileFunction",
|
||||
cached_data_status: "declared",
|
||||
cached_data: expect.objectContaining({
|
||||
source_path: null,
|
||||
source_expression: "loadCache()",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps compilations without a bytecode cache as compilations without declared cachedData", () => {
|
||||
const analysis = analyze(`
|
||||
const vm = require("node:vm");
|
||||
new vm.Script("./main.js");
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code).toEqual([
|
||||
expect.objectContaining({
|
||||
api: "vm.Script",
|
||||
cached_data_status: "absent",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
// A local object named `vm` is not the module, so its members name no VM API.
|
||||
it("ignores an unrelated local object that shares the module name", () => {
|
||||
const analysis = analyze(`
|
||||
const vm = { Script: class Script {} };
|
||||
new vm.Script("./main.js", { cachedData: "x" });
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code ?? []).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves unknown options and conservative constant-option reads without claiming execution", () => {
|
||||
for (const [source, status] of [
|
||||
['new vm.Script("x", options);', "unknown"],
|
||||
[
|
||||
'const options = { cachedData: "./cache" }; new vm.Script("x", options);',
|
||||
"declared",
|
||||
],
|
||||
['new vm.Script("x", { cachedData: value, ...options });', "unknown"],
|
||||
['new vm.Script("x", { ...options, cachedData: value });', "declared"],
|
||||
]) {
|
||||
const result = analyze('const vm = require("node:vm"); ' + source);
|
||||
expect(result.electron.dynamic_code?.[0], source).toMatchObject({
|
||||
cached_data_status: status,
|
||||
cached_data: { source_path: null },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps cache read authority unknown after overwriting the filesystem method", () => {
|
||||
const analysis = analyze(`
|
||||
const fs = require("node:fs");
|
||||
const vm = require("node:vm");
|
||||
fs.readFileSync = fakeReader;
|
||||
new vm.Script(source, { cachedData: fs.readFileSync("./not-a-native-cache") });
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code?.[0]?.cached_data).toMatchObject({
|
||||
source_path: null,
|
||||
source_resolution_context: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"new vm.Script(...args);",
|
||||
"new vm.Script(source, ...options);",
|
||||
"vm.compileFunction(...args);",
|
||||
"vm.compileFunction(source, ...args);",
|
||||
"vm.compileFunction(source, [], ...options);",
|
||||
])(
|
||||
"does not infer cachedData absence through argument spreads: %s",
|
||||
(source) => {
|
||||
const analysis = analyze('const vm = require("node:vm"); ' + source);
|
||||
expect(analysis.electron.dynamic_code?.[0]?.cached_data_status).toBe(
|
||||
"unknown",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['import * as vm from "node:vm";', 'import vm from "node:vm";'])(
|
||||
"recognizes ESM VM callable authority: %s",
|
||||
(declaration) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
declaration + " new vm.Script(source, { cachedData });",
|
||||
);
|
||||
expect(analysis.electron.dynamic_code?.[0]?.cached_data_status).toBe(
|
||||
"declared",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not report a replaced VM method as a built-in compilation", () => {
|
||||
const analysis = analyze(
|
||||
'const vm = require("node:vm"); vm.Script = Fake; new vm.Script(source, { cachedData });',
|
||||
);
|
||||
expect(analysis.electron.dynamic_code ?? []).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains awaited promise reads without treating an unawaited promise as cache bytes", () => {
|
||||
for (const [awaited, expected] of [
|
||||
["await ", "preload.bundle.cache"],
|
||||
["", null],
|
||||
] as const) {
|
||||
const analysis = analyze(`
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
async function load() {
|
||||
const cachedData = ${awaited}fs.readFile(path.join(__dirname, "preload.bundle.cache"));
|
||||
new vm.Script(source, { cachedData });
|
||||
}
|
||||
`);
|
||||
expect(analysis.electron.dynamic_code?.[0]?.cached_data.source_path).toBe(
|
||||
expected,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["void 0", "undefined"])(
|
||||
"keeps disabled cache value %s out of possible cache boundaries",
|
||||
(value) => {
|
||||
const analysis = analyze(
|
||||
`const vm = require("node:vm"); new vm.Script(source, { cachedData: ${value} }); vm.compileFunction(source, [], { cachedData: ${value} });`,
|
||||
);
|
||||
expect(
|
||||
analysis.electron.dynamic_code?.map(
|
||||
({ cached_data_status }) => cached_data_status,
|
||||
),
|
||||
).toEqual(["absent", "absent"]);
|
||||
expect(
|
||||
analysis.electron.dynamic_code?.[0]?.cached_data.source_expression,
|
||||
).toBe(value);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not confuse a shadowed undefined value with a disabled cache", () => {
|
||||
const analysis = analyze(
|
||||
'const vm = require("node:vm"); function load(undefined) { new vm.Script(source, { cachedData: undefined }); }',
|
||||
);
|
||||
expect(analysis.electron.dynamic_code?.[0]?.cached_data_status).toBe(
|
||||
"declared",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import { resolveElectronOptionObject } from "./electronStaticOptionObjects.js";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { compositeKey } from "../unicodeCodePointOrder.js";
|
||||
import type {
|
||||
ElectronDynamicCodeFinding,
|
||||
ElectronDynamicCodeSource,
|
||||
} from "./electronStaticAnalysisTypes.js";
|
||||
import {
|
||||
boundedExpression,
|
||||
electronStaticValue,
|
||||
} from "./electronStaticAnalysisValues.js";
|
||||
import { builtinDataEffectCallablePath } from "./javascriptSemanticDataEffectHelpers.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
staticObjectProperty,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
|
||||
import {
|
||||
argumentNode,
|
||||
range,
|
||||
staticFileExpressionPath,
|
||||
} from "./javascriptStaticAnalysisHelpers.js";
|
||||
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
|
||||
import {
|
||||
semanticStableInitializer,
|
||||
type JavaScriptBindingIndex,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
interface DynamicCodeInput {
|
||||
readonly context: JavaScriptFindingContext;
|
||||
readonly index: JavaScriptBindingIndex;
|
||||
readonly node: t.NewExpression | t.CallExpression;
|
||||
readonly api: ElectronDynamicCodeFinding["api"];
|
||||
readonly cachedDataArgument: "named" | "positional";
|
||||
}
|
||||
|
||||
/**
|
||||
* Record `vm.Script`/`vm.compileFunction` sites during the shared traversal.
|
||||
* Option objects may be rebound, so the `cachedData` lookup is deferred to the
|
||||
* same post-traversal step that settles the other Electron findings.
|
||||
*/
|
||||
export const deferElectronDynamicCode = (
|
||||
node: t.Node,
|
||||
context: JavaScriptFindingContext,
|
||||
): void => {
|
||||
// Identity waits for the settled index; this admits only the two syntax shapes
|
||||
// whose callee could name a `vm` member.
|
||||
const candidate =
|
||||
(t.isNewExpression(node) && syntacticMember(node.callee) === "Script") ||
|
||||
(t.isCallExpression(node) &&
|
||||
syntacticMember(node.callee) === "compileFunction");
|
||||
if (candidate) context.accumulator.pendingDynamicCode.push(node);
|
||||
};
|
||||
|
||||
const syntacticMember = (callee: t.Node): string | null => {
|
||||
const node = unwrapJavaScriptExpression(callee).node;
|
||||
return t.isMemberExpression(node) || t.isOptionalMemberExpression(node)
|
||||
? semanticStaticPropertyKey(node.property, node.computed)
|
||||
: null;
|
||||
};
|
||||
|
||||
/** Resolve the deferred `node:vm` compilations against the settled index. */
|
||||
export function* finishElectronDynamicCodeSteps(
|
||||
context: JavaScriptFindingContext,
|
||||
index: JavaScriptBindingIndex,
|
||||
): Generator<void, void> {
|
||||
const pending = context.accumulator.pendingDynamicCode;
|
||||
if (pending.length === 0) return;
|
||||
yield* index.indexMemberMutationFactsSteps();
|
||||
for (const node of pending) {
|
||||
const api = t.isNewExpression(node)
|
||||
? vmMemberAt(node.callee, index) === "Script"
|
||||
? "vm.Script"
|
||||
: undefined
|
||||
: vmMemberAt(node.callee, index) === "compileFunction"
|
||||
? "vm.compileFunction"
|
||||
: undefined;
|
||||
if (api === undefined) continue;
|
||||
const input: DynamicCodeInput = {
|
||||
context,
|
||||
index,
|
||||
node,
|
||||
api,
|
||||
cachedDataArgument: api === "vm.Script" ? "named" : "positional",
|
||||
};
|
||||
yield* addCompilation(input);
|
||||
}
|
||||
pending.length = 0;
|
||||
}
|
||||
|
||||
/** Resolve the complete callable through the shared built-in identity owner. */
|
||||
const vmMemberAt = (
|
||||
callee: t.Node,
|
||||
index: JavaScriptBindingIndex,
|
||||
): string | undefined => {
|
||||
const path = builtinDataEffectCallablePath(callee, index.bindings, ["vm"]);
|
||||
return path?.length === 1 ? path[0] : undefined;
|
||||
};
|
||||
|
||||
function* addCompilation(input: DynamicCodeInput): Generator<void, void> {
|
||||
const { context, node, api, cachedDataArgument } = input;
|
||||
const optionsPosition = cachedDataArgument === "named" ? 1 : 2;
|
||||
const spreadBeforeOptions = node.arguments
|
||||
.slice(0, optionsPosition + 1)
|
||||
.some((argument) => t.isSpreadElement(argument));
|
||||
const options = argumentNode(node.arguments[optionsPosition]);
|
||||
const lookup = spreadBeforeOptions
|
||||
? ({ status: "dynamic" } as const)
|
||||
: options === undefined
|
||||
? ({ status: "missing" } as const)
|
||||
: (() => {
|
||||
const resolved = resolveElectronOptionObject(
|
||||
options,
|
||||
options,
|
||||
input.index,
|
||||
["cachedData"],
|
||||
);
|
||||
return resolved === undefined
|
||||
? { status: "dynamic" as const }
|
||||
: staticObjectProperty(resolved, "cachedData");
|
||||
})();
|
||||
const disabled =
|
||||
lookup.status === "explicit" &&
|
||||
!t.isNullLiteral(unwrapJavaScriptExpression(lookup.value).node) &&
|
||||
context.expressionShapes.isNullish(lookup.value);
|
||||
const cachedData: ElectronDynamicCodeSource =
|
||||
lookup.status === "explicit"
|
||||
? sourceOf(input, lookup.value)
|
||||
: {
|
||||
source_path: null,
|
||||
source_resolution_context: null,
|
||||
source_expression:
|
||||
lookup.status === "dynamic"
|
||||
? "cachedData: [dynamic-options]"
|
||||
: "[absent]",
|
||||
};
|
||||
addLocatedFinding(context, {
|
||||
collection: context.accumulator.dynamicCode,
|
||||
key: compositeKey([
|
||||
"electron-dynamic-code",
|
||||
api,
|
||||
cachedData.source_path,
|
||||
cachedData.source_expression,
|
||||
]),
|
||||
node,
|
||||
value: {
|
||||
api,
|
||||
cached_data_status:
|
||||
lookup.status === "explicit"
|
||||
? disabled
|
||||
? "absent"
|
||||
: "declared"
|
||||
: lookup.status === "missing"
|
||||
? "absent"
|
||||
: "unknown",
|
||||
cached_data: cachedData,
|
||||
source_text: electronStaticValue(
|
||||
context.source,
|
||||
argumentNode(node.arguments[0]),
|
||||
),
|
||||
module_key: null,
|
||||
location: range(node),
|
||||
},
|
||||
});
|
||||
yield;
|
||||
}
|
||||
|
||||
const sourceOf = (
|
||||
input: DynamicCodeInput,
|
||||
node: t.Node,
|
||||
): ElectronDynamicCodeSource => {
|
||||
// `cachedData = fs.readFileSync(<path>)` names the cache through the read's
|
||||
// own argument; the call itself is not a path expression.
|
||||
const path = cachePathExpression(
|
||||
input.index,
|
||||
stableExpression(input.index, node) ?? node,
|
||||
);
|
||||
const fact =
|
||||
path === undefined
|
||||
? undefined
|
||||
: staticFileExpressionPath(path, input.context.pathIntrinsics);
|
||||
return fact === undefined
|
||||
? {
|
||||
source_path: null,
|
||||
source_resolution_context: null,
|
||||
source_expression: boundedExpression(input.context.source, node),
|
||||
}
|
||||
: {
|
||||
source_path: fact.path,
|
||||
source_path_base: fact.base,
|
||||
source_resolution_context: "filesystem-expression",
|
||||
source_expression: null,
|
||||
};
|
||||
};
|
||||
|
||||
/** The path argument of a proven `fs` read that produced the cache value. */
|
||||
const cachePathExpression = (
|
||||
index: JavaScriptBindingIndex,
|
||||
node: t.Node,
|
||||
): t.Node | undefined => {
|
||||
const value = unwrapJavaScriptExpression(node).node;
|
||||
const awaited = t.isAwaitExpression(value);
|
||||
const expression = awaited
|
||||
? unwrapJavaScriptExpression(value.argument).node
|
||||
: value;
|
||||
if (!t.isCallExpression(expression)) return undefined;
|
||||
const read = builtinDataEffectCallablePath(
|
||||
expression.callee,
|
||||
index.bindings,
|
||||
awaited ? ["fs/promises"] : ["fs"],
|
||||
);
|
||||
const target =
|
||||
read?.length === 1 && read[0] === (awaited ? "readFile" : "readFileSync")
|
||||
? argumentNode(expression.arguments[0])
|
||||
: undefined;
|
||||
return target === undefined
|
||||
? undefined
|
||||
: unwrapJavaScriptExpression(target).node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Follow one unrebound `const` to its sole initializer so a cache read into a
|
||||
* local name still names the artifact, exactly as the option-object owner does.
|
||||
*/
|
||||
const stableExpression = (
|
||||
index: JavaScriptBindingIndex,
|
||||
node: t.Node,
|
||||
): t.Node | undefined => {
|
||||
const expression = unwrapJavaScriptExpression(node).node;
|
||||
if (!t.isIdentifier(expression)) return undefined;
|
||||
const binding = index.bindingForReferenceAt(expression);
|
||||
if (binding?.mutable !== false) return undefined;
|
||||
const initializer = semanticStableInitializer(binding, index.bindings);
|
||||
return initializer === undefined
|
||||
? undefined
|
||||
: unwrapJavaScriptExpression(initializer.node).node;
|
||||
};
|
||||
@@ -276,6 +276,46 @@ export interface ElectronProtocolFinding {
|
||||
readonly location: JavaScriptSourceRange;
|
||||
}
|
||||
|
||||
/** Source of a script's bytes: an inert path expression or dynamic syntax. */
|
||||
export type ElectronDynamicCodeSource =
|
||||
| {
|
||||
readonly source_path: string;
|
||||
readonly source_path_base?:
|
||||
| "source-directory"
|
||||
| "literal-path"
|
||||
| undefined;
|
||||
readonly source_resolution_context: JavaScriptStaticPathContext;
|
||||
readonly source_expression: null;
|
||||
}
|
||||
| {
|
||||
readonly source_path: null;
|
||||
readonly source_resolution_context: null;
|
||||
readonly source_expression: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Source syntax for one node:vm compilation. Cache declaration does not establish
|
||||
* accepted bytes or runtime execution.
|
||||
*/
|
||||
export interface ElectronDynamicCodeFinding {
|
||||
readonly api: "vm.Script" | "vm.compileFunction";
|
||||
/**
|
||||
* Whether cachedData is declared, proven absent, or unresolved in options.
|
||||
*/
|
||||
readonly cached_data_status: "declared" | "absent" | "unknown";
|
||||
/** The option or positional argument naming the backing cache artifact. */
|
||||
readonly cached_data: ElectronDynamicCodeSource;
|
||||
/**
|
||||
* The source text argument when it is inert syntax, which is the case a
|
||||
* bytecode-cache handoff replaces at runtime.
|
||||
*/
|
||||
readonly source_text: ElectronStaticValue;
|
||||
readonly module_key: string | null;
|
||||
/** Recovered factory runtime; absent in older or unbundled findings. */
|
||||
readonly module_runtime?: string;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
}
|
||||
|
||||
/** All Electron-specific facts collected during one shared AST traversal. */
|
||||
export interface ElectronStaticFindings {
|
||||
/** Calls that may replace the application path used by Electron loadFile. */
|
||||
@@ -287,6 +327,8 @@ export interface ElectronStaticFindings {
|
||||
readonly utility_processes: readonly ElectronUtilityProcessFinding[];
|
||||
readonly native_addon_bindings: readonly ElectronNativeAddonBindingFinding[];
|
||||
readonly protocols: readonly ElectronProtocolFinding[];
|
||||
/** `node:vm` compilations whose code may come from an opaque bytecode cache. */
|
||||
readonly dynamic_code?: readonly ElectronDynamicCodeFinding[];
|
||||
/** Source-bound candidate paths through bundled exports and IPC closures. */
|
||||
readonly call_paths?: readonly ElectronCallPath[];
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ const electronBoundarySummarySchema = z.strictObject({
|
||||
resolved_native_addon_bindings: countSchema,
|
||||
/** Absent in evidence produced before protocol registrations were reported. */
|
||||
protocol_registrations: countSchema.exactOptional(),
|
||||
/** `node:vm` compilations whose logic may live in an opaque bytecode cache. */
|
||||
v8_cached_data_compilations: countSchema.exactOptional(),
|
||||
/**
|
||||
* Whether the Electron boundary surface was actually recovered. `empty` means
|
||||
* an analyzed source names Electron but no boundary fact was recovered, so the
|
||||
|
||||
@@ -22,7 +22,8 @@ export const estimateJavaScriptStaticApplicationProjection = (
|
||||
facts.electron.sender_validations.length +
|
||||
facts.electron.utility_processes.length +
|
||||
facts.electron.native_addon_bindings.length +
|
||||
facts.electron.protocols.length;
|
||||
facts.electron.protocols.length +
|
||||
(facts.electron.dynamic_code ?? []).length;
|
||||
let records = findings * 8;
|
||||
let repeatedBytes = 0;
|
||||
for (const registration of facts.bundler_registrations) {
|
||||
|
||||
@@ -725,6 +725,7 @@ const BUILTIN_RECEIVER_METHODS: Readonly<Record<string, ReadonlySet<string>>> =
|
||||
path: new Set(["resolve", "join", "dirname"]),
|
||||
"path/posix": new Set(["resolve", "join", "dirname"]),
|
||||
url: new Set(["fileURLToPath"]),
|
||||
vm: new Set(["compileFunction"]),
|
||||
fs: new Set([
|
||||
"readFile",
|
||||
"readFileSync",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
resolveElectronBrowserWindowReceiver,
|
||||
} from "./electronStaticRenderer.js";
|
||||
import { finishElectronNativeBindingsSteps } from "./electronStaticAnalysisNative.js";
|
||||
import { finishElectronDynamicCodeSteps } from "./electronStaticAnalysisDynamicCode.js";
|
||||
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
|
||||
import {
|
||||
hasPossibleCommonJsDelegateWrite,
|
||||
@@ -151,6 +152,7 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
finishBundlerExports(accumulator, index);
|
||||
yield* finishElectronNativeBindingsSteps(findings, index);
|
||||
yield* finishElectronBrowserWindowsSteps(findings, index);
|
||||
yield* finishElectronDynamicCodeSteps(findings, index);
|
||||
if (accumulator.pendingModuleRequireCalls.length > 0) {
|
||||
yield* index.indexMemberMutationFactsSteps();
|
||||
finishModuleRequireCalls(findings, index);
|
||||
@@ -365,6 +367,14 @@ const finalizeStaticAnalysis = (
|
||||
accumulator.nativeAddonBindings,
|
||||
accumulator.moduleRangeIndex,
|
||||
),
|
||||
...(accumulator.dynamicCode.length > 0
|
||||
? {
|
||||
dynamic_code: finalizeLocatedFindings(
|
||||
accumulator.dynamicCode,
|
||||
accumulator.moduleRangeIndex,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
protocols: finalizeLocatedFindings(
|
||||
accumulator.protocols,
|
||||
accumulator.moduleRangeIndex,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
ElectronNativeAddonBindingFinding,
|
||||
ElectronSenderValidationFinding,
|
||||
ElectronProtocolFinding,
|
||||
ElectronDynamicCodeFinding,
|
||||
ElectronUtilityProcessFinding,
|
||||
ElectronWindowConstructor,
|
||||
} from "./electronStaticAnalysisTypes.js";
|
||||
@@ -95,6 +96,8 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
| t.AssignmentExpression
|
||||
)[];
|
||||
readonly protocols: LocatedJavaScriptFinding<ElectronProtocolFinding>[];
|
||||
readonly dynamicCode: LocatedJavaScriptFinding<ElectronDynamicCodeFinding>[];
|
||||
readonly pendingDynamicCode: (t.NewExpression | t.CallExpression)[];
|
||||
readonly moduleRangeIndex: JavaScriptModuleRangeIndex;
|
||||
/** Binding identities proven as third-parameter loaders of recovered factories. */
|
||||
readonly factoryRequireBindingIds: Set<string>;
|
||||
@@ -201,6 +204,8 @@ export const createJavaScriptAnalysisAccumulator = (
|
||||
nativeAddonBindings: [],
|
||||
pendingNativeAddonBindings: [],
|
||||
protocols: [],
|
||||
dynamicCode: [],
|
||||
pendingDynamicCode: [],
|
||||
moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength),
|
||||
factoryRequireBindingIds: new Set(),
|
||||
seen: new Set(),
|
||||
|
||||
Reference in new issue
Block a user