mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(electron): keep literal IPC sends ambiguous beside unresolved handlers (#8120)
* fix(electron): keep literal IPC sends ambiguous beside unresolved handlers A main handler on an unresolved channel can receive any literal renderer send of a compatible mode, but pairing ignored it unless it had a scoped receiver. The send was then reported unpaired, or paired with another handler. Treat an unresolved-channel handler as a competitor that never pairs. The wrapper pass seeds only calls with a literal first argument, so it now records which invocations of a dynamic site it traced. The syntax finding gains `invocation_channels: "literal"` only when every invocation is a traced direct call of an unexported local function supplying a literal channel. Such a site is already represented by its per-invocation findings and does not compete. Fixes #8118 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): keep guarded wrapper registrations competing in IPC pairing A guarded wrapper registration keeps its predicate on a call path and has no IPC finding. It still counted toward `invocation_channels: "literal"`, so pairing lost it: a send on that channel was reported unpaired, or paired uniquely with a direct handler. The enumerated site now carries these channels as `guarded_invocation_channels`, and each one competes for its own channel without pairing. Index each binding's direct call sites by appending in place. Spreading the earlier calls on every reference was quadratic in a file's call count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
72039ecca9
commit
81378efb71
6 files changed
+317
-19
No files matched your search
@@ -138,7 +138,22 @@ export type ElectronIpcFinding = ElectronIpcDescriptor &
|
||||
ElectronIpcFindingContext &
|
||||
(
|
||||
| { readonly channel: string; readonly channel_expression: null }
|
||||
| { readonly channel: null; readonly channel_expression: string }
|
||||
| {
|
||||
readonly channel: null;
|
||||
readonly channel_expression: string;
|
||||
/**
|
||||
* Every invocation of this site is a traced direct call that supplied a
|
||||
* literal channel, recorded as its own wrapper-substitution finding or
|
||||
* guarded call path. Absent when any invocation may supply another value.
|
||||
*/
|
||||
readonly invocation_channels?: "literal";
|
||||
/**
|
||||
* Channels from guarded invocations, which keep their predicate on a
|
||||
* call path instead of becoming findings. Each still competes for its
|
||||
* channel in pairing.
|
||||
*/
|
||||
readonly guarded_invocation_channels?: readonly string[];
|
||||
}
|
||||
);
|
||||
|
||||
/** Static check involving an IPC sender, frame, process, URL, or origin. */
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticIdentifierRole } from "./javascriptSemanticProjection.js";
|
||||
import type { JavaScriptElectronIdentity } from "./javascriptSemanticAnalysis.js";
|
||||
@@ -138,7 +139,9 @@ const priorPath = (
|
||||
};
|
||||
|
||||
/** One physical IPC operation, independent of how its channel was resolved. */
|
||||
const ipcSiteKey = (value: ElectronIpcFinding): string =>
|
||||
const ipcSiteKey = (
|
||||
value: Pick<ElectronIpcFinding, "side" | "operation" | "location">,
|
||||
): string =>
|
||||
JSON.stringify([
|
||||
value.side,
|
||||
value.operation,
|
||||
@@ -432,6 +435,110 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
let ipcByEndpoint: Map<string, IpcEndpointFacts> | undefined;
|
||||
// Direct operations whose channel the syntax pass left unresolved, by site.
|
||||
let unresolvedIpcSites: Map<string, number> | undefined;
|
||||
// Traced invocation outcomes at IPC sites: the call sites of invocations
|
||||
// that supplied a literal channel, or any other outcome.
|
||||
const literalInvocationSites = new Map<
|
||||
string,
|
||||
{
|
||||
readonly node: t.Node;
|
||||
readonly calls: Set<string>;
|
||||
// Literals whose guarded registration has no IPC finding of its own.
|
||||
readonly guardedChannels: Set<string>;
|
||||
}
|
||||
>();
|
||||
const openInvocationSites = new Set<string>();
|
||||
// Runtime references to each binding: direct call sites, or null when any
|
||||
// other reference can invoke it from sites the trace never seeds.
|
||||
let directCalls:
|
||||
| Map<JavaScriptSemanticBindingState, t.CallExpression[] | null>
|
||||
| undefined;
|
||||
const directCallsTo = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): readonly t.CallExpression[] | null => {
|
||||
if (directCalls === undefined) {
|
||||
const references = new Map<
|
||||
JavaScriptSemanticBindingState,
|
||||
t.CallExpression[] | null
|
||||
>();
|
||||
traverseJavaScriptValueAst(program, state.erasedSyntaxNodes, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (
|
||||
!t.isIdentifier(node) ||
|
||||
parent === null ||
|
||||
semanticIdentifierRole(node, parent, readAncestors) === null
|
||||
)
|
||||
return;
|
||||
const referenced = resolveSemanticBindingState(
|
||||
state,
|
||||
node,
|
||||
node.name,
|
||||
);
|
||||
if (referenced === undefined) return;
|
||||
const calls = references.get(referenced);
|
||||
if (calls === null) return;
|
||||
if (!t.isCallExpression(parent) || parent.callee !== node)
|
||||
references.set(referenced, null);
|
||||
else if (calls === undefined) references.set(referenced, [parent]);
|
||||
else calls.push(parent);
|
||||
},
|
||||
});
|
||||
directCalls = references;
|
||||
}
|
||||
return directCalls.get(binding) ?? [];
|
||||
};
|
||||
/**
|
||||
* Whether every invocation of the function enclosing a node is a direct call
|
||||
* whose own invocation chain the trace followed. Seeds start only at literal
|
||||
* arguments, so an untraced call site may supply any channel.
|
||||
*/
|
||||
const tracedInvocationsOnly = (
|
||||
node: t.Node,
|
||||
traced: ReadonlySet<string>,
|
||||
active: ReadonlySet<t.Node> = new Set(),
|
||||
): boolean => {
|
||||
let fn = state.parentsByNode.get(node);
|
||||
while (fn !== undefined && !t.isFunction(fn))
|
||||
fn = state.parentsByNode.get(fn);
|
||||
// Top-level code runs once with the values the trace evaluated.
|
||||
if (fn === undefined) return true;
|
||||
if (active.has(fn)) return false;
|
||||
const declarator = state.parentsByNode.get(fn);
|
||||
const declaration = t.isFunctionDeclaration(fn)
|
||||
? fn
|
||||
: t.isVariableDeclarator(declarator) && declarator.init === fn
|
||||
? state.parentsByNode.get(declarator)
|
||||
: undefined;
|
||||
const owner =
|
||||
declaration === undefined
|
||||
? undefined
|
||||
: state.parentsByNode.get(declaration);
|
||||
// An exported declaration is invoked by importers this file never sees.
|
||||
if (
|
||||
declaration === undefined ||
|
||||
t.isExportNamedDeclaration(owner) ||
|
||||
t.isExportDefaultDeclaration(owner)
|
||||
)
|
||||
return false;
|
||||
const id = t.isFunctionDeclaration(fn)
|
||||
? fn.id
|
||||
: t.isVariableDeclarator(declarator) && t.isIdentifier(declarator.id)
|
||||
? declarator.id
|
||||
: undefined;
|
||||
const binding =
|
||||
id === undefined || id === null
|
||||
? undefined
|
||||
: resolveSemanticBindingState(state, id, id.name);
|
||||
const calls = binding === undefined ? null : directCallsTo(binding);
|
||||
return (
|
||||
calls !== null &&
|
||||
calls.length > 0 &&
|
||||
calls.every(
|
||||
(call) =>
|
||||
traced.has(ipcInvocationKey(range(call))) &&
|
||||
tracedInvocationsOnly(call, traced, new Set([...active, fn])),
|
||||
)
|
||||
);
|
||||
};
|
||||
const ipcAtEndpoint = (): Map<string, IpcEndpointFacts> => {
|
||||
if (ipcByEndpoint !== undefined) return ipcByEndpoint;
|
||||
ipcByEndpoint = new Map();
|
||||
@@ -825,18 +932,26 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
endpoint: string,
|
||||
guardedDispatch: boolean,
|
||||
): void => {
|
||||
const location = range(node);
|
||||
// The site evaluated in its own scope is not an invocation of it.
|
||||
const invocationSite =
|
||||
context.calls.length > 0
|
||||
? ipcSiteKey({ ...descriptor, location })
|
||||
: undefined;
|
||||
const open = (): void => {
|
||||
if (invocationSite !== undefined) openInvocationSites.add(invocationSite);
|
||||
};
|
||||
const first = args[0];
|
||||
if (first?.kind !== "literal" || typeof first.value !== "string") {
|
||||
accumulator.unknownFindings += 1;
|
||||
return;
|
||||
return open();
|
||||
}
|
||||
const channelContext = metadataContext(context, first);
|
||||
if (!channelContext) return;
|
||||
if (!channelContext) return open();
|
||||
const handlerValue = args[1] ?? symbolicUnknown("missing handler");
|
||||
const handlerContext = metadataContext(channelContext, handlerValue);
|
||||
if (!handlerContext) return;
|
||||
if (!handlerContext) return open();
|
||||
const channel = first.value;
|
||||
const location = range(node);
|
||||
const invocationLocation =
|
||||
javascriptSourceRangeKey(range(context.entryNode)) ===
|
||||
javascriptSourceRangeKey(location)
|
||||
@@ -936,6 +1051,21 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
: undefined;
|
||||
if (!findingGuarded && registrationKey !== undefined)
|
||||
unconditionalRegistrations.add(registrationKey);
|
||||
// A guarded registration keeps its predicate on the call path instead of
|
||||
// becoming an IPC finding, so the site carries its channel to pairing.
|
||||
const guardedOnly =
|
||||
findingGuarded &&
|
||||
(registrationKey === undefined ||
|
||||
!unconditionalRegistrations.has(registrationKey));
|
||||
if (invocationSite !== undefined) {
|
||||
let traced = literalInvocationSites.get(invocationSite);
|
||||
if (traced === undefined) {
|
||||
traced = { node, calls: new Set(), guardedChannels: new Set() };
|
||||
literalInvocationSites.set(invocationSite, traced);
|
||||
}
|
||||
for (const call of context.calls) traced.calls.add(call);
|
||||
if (guardedOnly) traced.guardedChannels.add(channel);
|
||||
}
|
||||
if (!duplicate && !findingGuarded) {
|
||||
const value: ElectronIpcFinding = {
|
||||
...finding,
|
||||
@@ -986,11 +1116,7 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
if (descriptor.mode !== "send" && descriptor.mode !== "invoke" && !fn)
|
||||
accumulator.unknownFindings += 1;
|
||||
}
|
||||
} else if (
|
||||
findingGuarded &&
|
||||
(registrationKey === undefined ||
|
||||
!unconditionalRegistrations.has(registrationKey))
|
||||
) {
|
||||
} else if (guardedOnly) {
|
||||
accumulator.unknownFindings += 1;
|
||||
// A guarded registration is a path candidate even when its callback has
|
||||
// no observable body. Keep its predicate instead of promoting it to an
|
||||
@@ -1435,7 +1561,34 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
if (remainingVisits < 0 || resolver.truncated) {
|
||||
accumulator.unknownFindings += 1;
|
||||
accumulator.electronCallPathLimitReached = true;
|
||||
}
|
||||
} else
|
||||
// A dynamic site's channels are enumerated only when the complete trace
|
||||
// reached it with literal channels, no other invocation outcome, and no
|
||||
// way to invoke its wrapper from sites the trace does not seed.
|
||||
for (const [site, index] of unresolvedIpcSites ?? []) {
|
||||
const entry = accumulator.ipc[index];
|
||||
const traced = literalInvocationSites.get(site);
|
||||
if (
|
||||
entry?.value.channel === null &&
|
||||
traced !== undefined &&
|
||||
!openInvocationSites.has(site) &&
|
||||
tracedInvocationsOnly(traced.node, traced.calls)
|
||||
)
|
||||
accumulator.ipc[index] = {
|
||||
...entry,
|
||||
value: {
|
||||
...entry.value,
|
||||
invocation_channels: "literal",
|
||||
...(traced.guardedChannels.size === 0
|
||||
? {}
|
||||
: {
|
||||
guarded_invocation_channels: [...traced.guardedChannels].sort(
|
||||
compareUnicodeCodePoints,
|
||||
),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (resolver.unresolved) accumulator.unknownFindings += 1;
|
||||
return paths;
|
||||
}
|
||||
Reference in new issue
Block a user