mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +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
@@ -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"
|
||||
? []
|
||||
: [
|
||||
|
||||
Reference in new issue
Block a user