Merge remote-tracking branch 'origin/main' into codex/bug-sweep-20261015-final

This commit is contained in:
morluto committed 2026-10-15 09:01:38 +08:00
commit 610f873e91
8 files changed
+220 -187

No files matched your search

@@ -5,10 +5,7 @@ import { setImmediate } from "node:timers/promises";
import { resolveJavaScriptSourceMapReferences } from "../../domain/javascript/javascriptSourceMapPaths.js";
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptOpenReceiversSteps,
} from "../../domain/javascript/javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import { projectJavaScriptModuleSemantics } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
@@ -382,12 +379,9 @@ function* analyzeArtifactFileSteps<
state.parseFailures += 1;
return;
}
const openReceiverFacts =
yield* classifyParsedJavaScriptOpenReceiversSteps(parsed);
const analysis = yield* analyzeParsedJavaScriptStaticSourceSteps(
file.text.value,
parsed,
openReceiverFacts,
);
const staticFindings = findingCount(analysis);
yield;
@@ -1,10 +1,7 @@
import { moduleResolutionModeForJavaScriptReference } from "../domain/javascript/javascriptModuleResolutionMode.js";
import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js";
import { analyzeParsedJavaScriptStaticSourceSteps } from "../domain/javascript/javascriptStaticAnalysis.js";
import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptOpenReceiversSteps,
} from "../domain/javascript/javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptSemanticsSteps } from "../domain/javascript/javascriptSemanticAnalysis.js";
import { failedJavaScriptStaticAnalysis } from "../domain/javascript/javascriptStaticAnalysisHelpers.js";
import { projectJavaScriptModuleSemantics } from "../domain/javascript/javascriptModuleSemanticIr.js";
import {
@@ -33,15 +30,8 @@ const analyze: JavaScriptSourceWorkerAnalyzer = async (
return null;
}
await callbacks.phase("static");
const receivers = await completeJavaScriptAnalysisSteps(
classifyParsedJavaScriptOpenReceiversSteps(parsed),
);
const facts = await completeJavaScriptAnalysisSteps(
analyzeParsedJavaScriptStaticSourceSteps(
file.text.value,
parsed,
receivers,
),
analyzeParsedJavaScriptStaticSourceSteps(file.text.value, parsed),
);
const resultBudgetBytes = await callbacks.checkpoint(facts);
if (facts.parse_status === "failed") return null;
@@ -353,6 +353,22 @@ it("retains distinct wrapper invocation ranges for one IPC API source", () => {
findings[1]?.invocation_location,
);
});
it("keeps one wrapper registration when one invocation reaches it twice", () => {
const result = analyzeJavaScriptStaticSource(`
const e=require("electron");
const copy=event=>event.sender.copy();
function register(channel,handler){e.ipcMain.on(channel,handler);}
function twice(channel){register(channel,copy);register(channel,copy);}
twice("same:invocation");
`);
expect(
result.electron.ipc.filter(
({ resolution_basis, channel }) =>
resolution_basis === "wrapper-substitution" &&
channel === "same:invocation",
),
).toHaveLength(1);
});
it("keeps a directly observed renderer operation singular when tracing its exposed callback", () => {
const result = analyzeJavaScriptStaticSource(`
const {contextBridge,ipcRenderer}=require("electron");
@@ -43,6 +43,19 @@ const callLabel = (node: t.Node): string =>
? callLabel(node.expressions.at(-1) ?? node)
: calleeName(node) || "local call";
const ipcEndpointKey = (value: ElectronIpcFinding): string =>
JSON.stringify([value.side, value.operation, value.channel, value.location]);
const indexIpcFinding = (
index: Map<string, ElectronIpcFinding[]>,
value: ElectronIpcFinding,
): void => {
const key = ipcEndpointKey(value);
const bucket = index.get(key);
if (bucket === undefined) index.set(key, [value]);
else bucket.push(value);
};
interface PathContext {
readonly entryNode: t.Node;
readonly entry: string;
@@ -149,6 +162,16 @@ export const collectElectronWrappedCallPaths = (
) ?? context
);
};
// Duplicate checks consult only facts at the same endpoint and call site.
// Scanning every prior fact made bundles with many registrations quadratic.
let ipcByEndpoint: Map<string, ElectronIpcFinding[]> | undefined;
const ipcAtEndpoint = (): Map<string, ElectronIpcFinding[]> => {
if (ipcByEndpoint !== undefined) return ipcByEndpoint;
ipcByEndpoint = new Map();
for (const { value } of accumulator.ipc)
indexIpcFinding(ipcByEndpoint, value);
return ipcByEndpoint;
};
const hasConditions = (value: SymbolicValue): boolean =>
symbolicVariants(value).some((variant) => variant.conditions?.length);
function* bridgeMemberFunctions(
@@ -351,24 +374,22 @@ export const collectElectronWrappedCallPaths = (
};
// A fully observed transmission stays one source fact when its callback is
// traced. Registrations retain invocation identity because handlers compete.
const duplicate = accumulator.ipc.some(
({ value }) =>
value.side === finding.side &&
value.operation === finding.operation &&
value.channel === channel &&
JSON.stringify(value.location) === JSON.stringify(location) &&
((value.resolution_basis === undefined &&
const invocationKey = JSON.stringify(invocationLocation);
const duplicate = (ipcAtEndpoint().get(ipcEndpointKey(finding)) ?? []).some(
(value) =>
(value.resolution_basis === undefined &&
(value.mode === "send" || value.mode === "invoke")) ||
JSON.stringify(value.invocation_location) ===
JSON.stringify(invocationLocation)),
JSON.stringify(value.invocation_location) === invocationKey,
);
const findingGuarded =
context.conditions.length > 0 || hasConditions(first) || guardedDispatch;
if (!duplicate && !findingGuarded) {
accumulator.ipc.push({
offset: node.start ?? -1,
value: { ...finding, resolution_basis: "wrapper-substitution" },
});
const value: ElectronIpcFinding = {
...finding,
resolution_basis: "wrapper-substitution",
};
accumulator.ipc.push({ offset: node.start ?? -1, value });
indexIpcFinding(ipcAtEndpoint(), value);
if (descriptor.mode !== "send" && descriptor.mode !== "invoke" && !fn)
accumulator.unknownFindings += 1;
} else if (findingGuarded) accumulator.unknownFindings += 1;
@@ -223,37 +223,6 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
};
}
/** Resolve tRPC client roots by lexical identity, excluding overwritten bindings. */
export function* classifyParsedJavaScriptRpcClientRootsSteps(
file: ParsedJavaScriptSource,
): Generator<void, ReadonlySet<t.Node>> {
const state = createState(file.program);
yield* collectDefinitionsSteps(file.program, state);
const roots = new Set<t.Node>();
const clients = new Map<string, boolean>();
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => {
if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node))
return;
const root = node.object;
if (!t.isIdentifier(root)) return;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return;
let client = clients.get(binding.bindingId);
if (client === undefined) {
const initializer = semanticStableInitializer(binding, state);
client =
initializer !== undefined &&
initializer.projection.length === 0 &&
isJavaScriptRpcClientFactory(initializer.node);
clients.set(binding.bindingId, client);
}
if (client) roots.add(root);
},
});
return roots;
}
/**
* Receiver identity facts for the overloaded `.open` syntax, plus
* `configuration` for `.get` reads of a `getConfiguration(...)` result.
@@ -268,111 +237,169 @@ export type JavaScriptOpenReceiverFact =
| "configuration"
| "extension-state";
/**
* Resolve only the lexical receiver identity needed to distinguish browser
* `window.open` from XHR and global storage `.open` calls, and configuration
* `.get` reads from HTTP client calls. This deliberately builds definitions
* without running semantic value or provenance analysis.
*/
export const classifyParsedJavaScriptOpenReceivers = (
file: ParsedJavaScriptSource,
): ReadonlyMap<number, JavaScriptOpenReceiverFact> =>
completeSemanticSteps(classifyParsedJavaScriptOpenReceiversSteps(file));
/** Lexical receiver identities that select static endpoint interpretations. */
export interface JavaScriptReceiverFacts {
readonly openReceivers: ReadonlyMap<number, JavaScriptOpenReceiverFact>;
readonly rpcClientRoots: ReadonlySet<t.Node>;
}
/** Classify `.open` receivers while yielding during lexical and AST scans. */
export function* classifyParsedJavaScriptOpenReceiversSteps(
/**
* Resolve only the lexical receiver identities needed to distinguish browser
* `window.open` from XHR and global storage `.open` calls, configuration
* `.get` reads from HTTP client calls, and tRPC client roots (excluding
* overwritten bindings). This deliberately builds definitions without running
* semantic value or provenance analysis.
*/
export const classifyParsedJavaScriptReceivers = (
file: ParsedJavaScriptSource,
): Generator<void, ReadonlyMap<number, JavaScriptOpenReceiverFact>> {
): JavaScriptReceiverFacts =>
completeSemanticSteps(classifyParsedJavaScriptReceiversSteps(file));
/** Classify receivers while yielding during lexical and AST scans. */
export function* classifyParsedJavaScriptReceiversSteps(
file: ParsedJavaScriptSource,
): Generator<void, JavaScriptReceiverFacts> {
const state = createState(file.program);
yield* collectDefinitionsSteps(file.program, state);
return yield* classifyDefinitionReceiversSteps(file.program, state);
}
/**
* Index bindings and classify receivers from one definitions pass. Receiver
* facts read the definitions-only phase, so they are classified before the
* same state is extended with ambient writes.
*/
export function* indexJavaScriptBindingsWithReceiversSteps(
program: t.Program,
): Generator<
void,
{
readonly bindings: JavaScriptSemanticAnalysisState;
readonly receivers: JavaScriptReceiverFacts;
}
> {
const state = createState(program);
yield* collectDefinitionsSteps(program, state);
const receivers = yield* classifyDefinitionReceiversSteps(program, state);
yield* indexJavaScriptAmbientWritesSteps(program, state);
return { bindings: state, receivers };
}
function* classifyDefinitionReceiversSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): Generator<void, JavaScriptReceiverFacts> {
const facts = new Map<number, JavaScriptOpenReceiverFact>();
const extensionStateReads: number[] = [];
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => {
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
(!t.isMemberExpression(node.callee) &&
!t.isOptionalMemberExpression(node.callee))
)
return;
const method = semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
const roots = new Set<t.Node>();
const clients = new Map<string, boolean>();
const classifyRpcRoot = (node: t.Node): void => {
if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node))
return;
const root = node.object;
if (!t.isIdentifier(root)) return;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return;
let client = clients.get(binding.bindingId);
if (client === undefined) {
const initializer = semanticStableInitializer(binding, state);
client =
initializer !== undefined &&
initializer.projection.length === 0 &&
isJavaScriptRpcClientFactory(initializer.node);
clients.set(binding.bindingId, client);
}
if (client) roots.add(root);
};
const classifyOpenReceiver = (node: t.Node): void => {
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
(!t.isMemberExpression(node.callee) &&
!t.isOptionalMemberExpression(node.callee))
)
return;
const method = semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
);
if (method === "get") {
if (isConfigurationReceiver(state, node.callee.object))
facts.set(node.start ?? -1, "configuration");
else if (isExtensionStateMember(node.callee.object))
extensionStateReads.push(node.start ?? -1);
return;
}
if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
const receiver = node.callee.object.name;
const binding = resolveSemanticBindingState(state, node, receiver);
if (binding === undefined) {
if (semanticResolutionBlocked(state, node, receiver)) return;
const fact = openGlobalFact(receiver);
if (fact !== undefined) facts.set(node.start ?? -1, fact);
return;
}
// A lexically bound receiver overrides ambient browser globals. Mark an
// otherwise unresolved local as local so name-based window.open
// heuristics do not hide its XHR-like overloads.
facts.set(node.start ?? -1, "local");
if (receiver === "indexedDB" || receiver === "caches") {
facts.set(
node.start ?? -1,
receiver === "indexedDB" ? "local-indexed-db" : "local-cache-storage",
);
if (method === "get") {
if (isConfigurationReceiver(state, node.callee.object))
facts.set(node.start ?? -1, "configuration");
else if (isExtensionStateMember(node.callee.object))
extensionStateReads.push(node.start ?? -1);
return;
}
if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
const receiver = node.callee.object.name;
const binding = resolveSemanticBindingState(state, node, receiver);
if (binding === undefined) {
if (semanticResolutionBlocked(state, node, receiver)) return;
const fact = openGlobalFact(receiver);
if (fact !== undefined) facts.set(node.start ?? -1, fact);
return;
}
// A lexically bound receiver overrides ambient browser globals. Mark an
// otherwise unresolved local as local so name-based window.open
// heuristics do not hide its XHR-like overloads.
facts.set(node.start ?? -1, "local");
if (receiver === "indexedDB" || receiver === "caches") {
facts.set(
node.start ?? -1,
receiver === "indexedDB" ? "local-indexed-db" : "local-cache-storage",
);
return;
}
if (binding.mutable || binding.initializers.length !== 1) return;
const initializer = binding.initializers[0]?.node;
if (!t.isIdentifier(initializer)) return;
const initializerBinding = resolveSemanticBindingState(
state,
initializer,
initializer.name,
);
if (initializerBinding !== undefined) {
if (initializer.name === "indexedDB" || initializer.name === "caches") {
const localFact =
initializer.name === "indexedDB"
? "local-indexed-db"
: "local-cache-storage";
facts.set(node.start ?? -1, localFact);
if (
initializerBinding.mutable ||
initializerBinding.initializers.length !== 1
)
return;
const nested = initializerBinding.initializers[0]?.node;
if (t.isIdentifier(nested)) {
const nestedFact = resolveGlobalAliasFact(
state,
initializer,
nested.name,
);
if (nestedFact !== undefined)
facts.set(node.start ?? -1, nestedFact);
}
} else if (initializer.name === "window") {
facts.set(node.start ?? -1, "local");
return;
}
if (binding.mutable || binding.initializers.length !== 1) return;
const initializer = binding.initializers[0]?.node;
if (!t.isIdentifier(initializer)) return;
const initializerBinding = resolveSemanticBindingState(
state,
initializer,
initializer.name,
);
if (initializerBinding !== undefined) {
if (initializer.name === "indexedDB" || initializer.name === "caches") {
const localFact =
initializer.name === "indexedDB"
? "local-indexed-db"
: "local-cache-storage";
facts.set(node.start ?? -1, localFact);
if (
initializerBinding.mutable ||
initializerBinding.initializers.length !== 1
)
return;
const nested = initializerBinding.initializers[0]?.node;
if (t.isIdentifier(nested)) {
const nestedFact = resolveGlobalAliasFact(
state,
initializer,
nested.name,
);
if (nestedFact !== undefined) facts.set(node.start ?? -1, nestedFact);
}
return;
} else if (initializer.name === "window") {
facts.set(node.start ?? -1, "local");
}
const aliasFact = resolveGlobalAliasFact(
state,
initializer,
initializer.name,
);
if (aliasFact !== undefined) facts.set(node.start ?? -1, aliasFact);
return;
}
const aliasFact = resolveGlobalAliasFact(
state,
initializer,
initializer.name,
);
if (aliasFact !== undefined) facts.set(node.start ?? -1, aliasFact);
};
yield* traverseJavaScriptAstSteps(program, {
enter: (node) => {
classifyRpcRoot(node);
classifyOpenReceiver(node);
},
});
if (extensionStateReads.length > 0 && importsVsCodeModule(state))
for (const start of extensionStateReads)
facts.set(start, "extension-state");
return facts;
return { openReceivers: facts, rpcClientRoots: roots };
}
/** VS Code passes extension state through ExtensionContext, not an import. */
@@ -5,8 +5,8 @@ import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptElectronBindingsSteps,
classifyParsedJavaScriptOpenReceivers,
classifyParsedJavaScriptOpenReceiversSteps,
classifyParsedJavaScriptReceivers,
classifyParsedJavaScriptReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import {
@@ -110,15 +110,15 @@ new NativeWindow();
`;
const parsed = parseJavaScriptSource(text, "receivers.js");
if (parsed === null) throw new Error("Expected parsed source");
const openSteps = classifyParsedJavaScriptOpenReceiversSteps(parsed);
const openSteps = classifyParsedJavaScriptReceiversSteps(parsed);
let openStep = openSteps.next();
expect(openStep.done).toBe(false);
while (openStep.done !== true) openStep = openSteps.next();
expect([...openStep.value]).toEqual([
expect([...openStep.value.openReceivers]).toEqual([
[text.indexOf('ambient.open("/local")'), "local"],
[text.indexOf('ambient.open("/global")'), "window"],
]);
expect(openStep.value).toEqual(classifyParsedJavaScriptOpenReceivers(parsed));
expect(openStep.value).toEqual(classifyParsedJavaScriptReceivers(parsed));
const electronSteps = classifyParsedJavaScriptElectronBindingsSteps(parsed);
let electronStep = electronSteps.next();
@@ -42,10 +42,7 @@ import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.j
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
import {
indexJavaScriptElectronIdentitySteps,
indexJavaScriptBindingsSteps,
classifyParsedJavaScriptRpcClientRootsSteps,
classifyParsedJavaScriptOpenReceivers,
type JavaScriptOpenReceiverFact,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import {
parseJavaScriptSource,
@@ -67,45 +64,38 @@ export const analyzeJavaScriptStaticSource = (
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptStaticAnalysis()
: analyzeParsedJavaScriptStaticSource(
source,
file,
classifyParsedJavaScriptOpenReceivers(file),
);
: analyzeParsedJavaScriptStaticSource(source, file);
};
/** Recover static structure from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptStaticSource = (
source: string,
file: ParsedJavaScriptSource,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): JavaScriptStaticAnalysis =>
completeSemanticSteps(
analyzeParsedJavaScriptStaticSourceSteps(source, file, openReceiverFacts),
);
completeSemanticSteps(analyzeParsedJavaScriptStaticSourceSteps(source, file));
/** Static analysis that yields during its source traversal; results are unchanged. */
export function* analyzeParsedJavaScriptStaticSourceSteps(
source: string,
file: ParsedJavaScriptSource,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): Generator<void, JavaScriptStaticAnalysis> {
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
const bindings = yield* indexJavaScriptBindingsSteps(file.program);
// One definitions pass serves receiver classification and binding lookup.
const { bindings, receivers } =
yield* indexJavaScriptBindingsWithReceiversSteps(file.program);
const electronIdentity = yield* indexJavaScriptElectronIdentitySteps(
file.program,
bindings,
);
const clientRoots = yield* classifyParsedJavaScriptRpcClientRootsSteps(file);
yield* traverseStaticSourceSteps(
file,
{
source,
accumulator,
vitePreloadDependencyTable: indexVitePreloadDependencyTable(file.program),
...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
openReceiverFacts: receivers.openReceivers,
electronBindings: electronIdentity.bindings,
rpcScan: createJavaScriptRpcScan(source, clientRoots),
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots),
},
bindings,
);
+5 -10
View File
@@ -4,14 +4,8 @@ import { sanitizeEndpointCandidate } from "./browserObservation.js";
import { compositeKey } from "./unicodeCodePointOrder.js";
import type { WebPageInspection } from "./browserObservationSchemas.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascript/javascriptSemanticTraversal.js";
import {
classifyParsedJavaScriptOpenReceivers,
classifyParsedJavaScriptRpcClientRootsSteps,
} from "./javascript/javascriptSemanticAnalysis.js";
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
import { classifyParsedJavaScriptReceivers } from "./javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js";
import { semanticStaticPropertyName } from "./javascript/javascriptAstValues.js";
import {
@@ -67,10 +61,11 @@ export const analyzeScript = (
if ((file.errors?.length ?? 0) > 0)
accumulator.recoveredScripts.push(script.script_key);
detectVendorFingerprints(script, accumulator);
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(file);
const receivers = classifyParsedJavaScriptReceivers(file);
const openReceiverFacts = receivers.openReceivers;
const rpc = createJavaScriptRpcScan(
script.source.artifact.text,
completeSemanticSteps(classifyParsedJavaScriptRpcClientRootsSteps(file)),
receivers.rpcClientRoots,
);
traverseJavaScriptAst(file, {
enter: (node) => {