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:
authored and GitHub committed 2026-11-09 20:16:18 +08:00
1 parent ab1b9e35d0
commit 2c27c2a43b
21 files changed
+982 -4

No files matched your search

@@ -1,3 +1,4 @@
import { isPossibleCachedDataCompilation } from "../../domain/javascript/electronDynamicCodeFacts.js";
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
@@ -200,7 +201,10 @@ export const electronBoundaryFactCount = (
javascript.electron.native_addon_bindings.filter(
provenElectronFinding,
).length +
javascript.electron.protocols.filter(provenElectronFinding).length),
javascript.electron.protocols.filter(provenElectronFinding).length +
(javascript.electron.dynamic_code ?? []).filter(
isPossibleCachedDataCompilation,
).length),
0,
);
@@ -398,6 +402,14 @@ export const summarizeElectronBoundaries = (
}),
).length,
native_addon_bindings: nativeBindings.length,
v8_cached_data_compilations: javascript.reduce(
(count, { electron }) =>
count +
(electron.dynamic_code ?? []).filter(
({ cached_data_status }) => cached_data_status === "declared",
).length,
0,
),
resolved_native_addon_bindings: nativeBindings.filter(({ file, finding }) =>
resolvesToFile({
file,
@@ -3,6 +3,7 @@ import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphCo
import { addElectronIpcBoundaries } from "./ElectronBoundaryGraphIpc.js";
import { addElectronNativeBoundaries } from "./ElectronBoundaryGraphNative.js";
import { addElectronProtocolBoundaries } from "./ElectronBoundaryGraphProtocols.js";
import { addElectronDynamicCodeBoundaries } from "./ElectronBoundaryGraphDynamicCode.js";
import { addElectronWindowBoundaries } from "./ElectronBoundaryGraphWindows.js";
import { addElectronCallPaths } from "./ElectronBoundaryGraphCallPaths.js";
@@ -15,5 +16,6 @@ export const addElectronBoundaries = (
addElectronIpcBoundaries(context, ipc);
addElectronNativeBoundaries(context);
addElectronProtocolBoundaries(context);
addElectronDynamicCodeBoundaries(context);
addElectronCallPaths(context, ipc);
};
@@ -0,0 +1,156 @@
import { isPossibleCachedDataCompilation } from "../../domain/javascript/electronDynamicCodeFacts.js";
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
import type {
ElectronDynamicCodeFinding,
ElectronDynamicCodeSource,
} from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
javascriptAnalysisCoverage,
type JavaScriptArtifactGraphContext,
type JavaScriptArtifactGraphCoverage,
} from "./JavaScriptArtifactGraphContext.js";
import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
import {
addElectronInferenceEdge,
electronFindingSourceNode,
electronObservationIdentity,
} from "./ElectronBoundaryGraphContext.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
interface DynamicCodeInput {
readonly context: JavaScriptArtifactGraphContext;
readonly file: JavaScriptArtifactFile;
readonly value: ElectronDynamicCodeFinding;
readonly coverage: JavaScriptArtifactGraphCoverage;
}
const CACHED_DATA_COMPILATION_LIMITATION =
"Source syntax declares or may supply cachedData to a V8 compilation API. Static analysis does not establish valid cache bytes, V8 acceptance, or execution; logic inside a possible cache remains unknown.";
/**
* Project `node:vm` compilations. A `cachedData` handoff becomes an explicit
* unknown carrying the inventoried cache artifact, so a thin wrapper whose
* preload logic lives in a bytecode cache is not read as an empty preload.
*/
export const addElectronDynamicCodeBoundaries = (
context: JavaScriptArtifactGraphContext,
): void => {
for (const analyzed of context.analysis.files) {
const { file, javascript } = analyzed;
if (javascript === null) continue;
const coverage = javascriptAnalysisCoverage(javascript);
for (const value of javascript.electron.dynamic_code ?? [])
addDynamicCodeSite({ context, file, value, coverage });
}
};
const addDynamicCodeSite = (input: DynamicCodeInput): void => {
const { context, file, value, coverage } = input;
if (!isPossibleCachedDataCompilation(value)) return;
const source = electronFindingSourceNode(
context,
file,
value.module_key,
value.module_runtime,
);
if (source === undefined) return;
const cache = value.cached_data;
const resolved = resolveCache(context, file, cache);
const node = context.accumulator.addNode({
kind: "unknown",
identity: electronObservationIdentity(
context,
"javascript-v8-cached-data",
compositeKey([
file.path,
value.api,
cache.source_path,
cache.source_expression,
javascriptSourceRangeKey(value.location),
]),
),
observations: [
{
label: cache.source_path ?? cache.source_expression,
properties: {
api: value.api,
cached_data_status: value.cached_data_status,
cache_path: cache.source_path,
cache_expression: cache.source_expression,
cache_path_base:
cache.source_path === null
? null
: (cache.source_path_base ?? null),
resolved_cache_path: resolved?.resolved_path ?? null,
resolution_status: resolved?.resolution_status ?? null,
resolution_context: resolved?.resolution_context ?? null,
candidate_cache_path: resolved?.candidate_path ?? null,
resolution_limitations: resolved?.limitations ?? [],
source_text: value.source_text.value,
source_expression: value.source_text.expression,
compilation_boundary: "v8-cached-data",
},
evidence: astObservationEvidence({
sha256: file.sha256,
path: file.path,
range: value.location,
operation: "observe-v8-cached-data-compilation",
coverage,
limitations: [CACHED_DATA_COMPILATION_LIMITATION],
}),
},
],
});
addElectronInferenceEdge(context, {
source,
target: node,
file,
range: value.location,
coverage,
relation: "loads",
operation: "associate-v8-cached-data-compilation",
properties: {
api: value.api,
cache_path: cache.source_path,
cache_expression: cache.source_expression,
cache_path_base:
cache.source_path === null ? null : (cache.source_path_base ?? null),
resolved_cache_path: resolved?.resolved_path ?? null,
resolution_status: resolved?.resolution_status ?? null,
resolution_context: resolved?.resolution_context ?? null,
candidate_cache_path: resolved?.candidate_path ?? null,
resolution_limitations: resolved?.limitations ?? [],
},
limitations: [CACHED_DATA_COMPILATION_LIMITATION],
});
};
/**
* The cache artifact the handoff names, when the path is inert syntax. The
* cache often is not a file this analysis reads, so lookup uses the full
* inventory map rather than the relevant-file projection.
*/
const resolveCache = (
context: JavaScriptArtifactGraphContext,
file: JavaScriptArtifactFile,
cache: ElectronDynamicCodeSource,
) => {
if (
cache.source_path === null ||
cache.source_path_base !== "source-directory"
)
return null;
const resolved = resolveArtifactPathByContext({
declaredPath: cache.source_path,
sourcePath: file.path,
context: cache.source_resolution_context,
files: context.filesByPath,
jsonDocuments: context.analysis.jsonDocuments,
directoryPaths: context.directoryPaths,
unanalyzedFiles: context.unanalyzedFiles,
runtimePathKind: "exact-file",
});
return resolved;
};
@@ -775,7 +775,8 @@ const findingCount = (analysis: JavaScriptStaticAnalysis): number =>
analysis.electron.sender_validations.length +
analysis.electron.utility_processes.length +
analysis.electron.native_addon_bindings.length +
analysis.electron.protocols.length;
analysis.electron.protocols.length +
(analysis.electron.dynamic_code ?? []).length;
const optionalString = (value: unknown): string | null =>
typeof value === "string" && value.length > 0 ? value : null;
@@ -1,3 +1,4 @@
import { isPossibleCachedDataCompilation } from "../../domain/javascript/electronDynamicCodeFacts.js";
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import {
electronIncompleteSourceCount,
@@ -434,6 +435,15 @@ const graphLimitations = (
"Native member names are requested by JavaScript syntax and are not verified binary exports in this workflow.",
]
: []),
...(context.analysis.files.some(({ javascript }) =>
(javascript?.electron.dynamic_code ?? []).some(
isPossibleCachedDataCompilation,
),
)
? [
"A node:vm compilation may receive cachedData; static syntax does not establish cache acceptance or execution, and possible cached logic remains unknown rather than absent.",
]
: []),
...(coverage === "complete"
? []
: [