fix(javascript): preserve bound receivers for intrinsic invocation
This commit is contained in:
1 parent
1e1a611012
commit
3e9bce6249
6 files changed
+95
-70
No files matched your search
@@ -344,7 +344,8 @@ reassignments, dynamic selections, and visibly overwritten namespace members
|
||||
are unresolved.
|
||||
Known Object/Reflect mutators remain recognizable through literal destructuring
|
||||
of intrinsic objects and stable bound-function aliases. Calls retain bound
|
||||
argument prefixes through `call`, `apply`, and `Reflect.apply`; unexamined
|
||||
argument prefixes and the original bound receiver through `call`, `apply`, and
|
||||
`Reflect.apply`, including generic `Function.prototype` invocation; unexamined
|
||||
targets remain unresolved. Creating an unused binding does not itself mutate
|
||||
an intrinsic. Variable redeclarations retain a single unconditional initializer
|
||||
when they introduce no assignment or second initializer.
|
||||
|
||||
@@ -546,6 +546,12 @@ describe("Electron module interop bound mutators", () => {
|
||||
'const change = Object.defineProperty.bind(null,...args); change(Object,"create",{value:customCreate});',
|
||||
'const metadata = {}; Object.defineProperty.call(...args,metadata,"x",{value:1});',
|
||||
'const metadata = {}; Object.defineProperty.apply(...args,[metadata,"x",{value:1}]);',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
'const first = Function.prototype.call.bind(Object.defineProperty,null); const change = first.bind(customReceiver,Object,"create"); change({value:customCreate});',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty,null); change([Object,"create",{value:customCreate}]);',
|
||||
'Function.prototype.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'Function.prototype.apply.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'const uncurry = Function.prototype.call.bind(Function.prototype.call); uncurry(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'const change = Reflect.set.bind(null,Object,"create"); change(customCreate);',
|
||||
'const change = Object.defineProperty.call; change(null,Object,"create",{value:customCreate});',
|
||||
'const {apply:change} = Object.defineProperty; change(null,[Object,"create",{value:customCreate}]);',
|
||||
@@ -562,6 +568,8 @@ describe("Electron module interop bound mutators", () => {
|
||||
'const change = Object.defineProperty.bind(null); change(metadata,"x",{value:1});',
|
||||
'const change = Object.defineProperty.bind(null,metadata,"x"); change({value:1});',
|
||||
'const first = Object.defineProperty.bind(null,metadata); const change = first.bind(null,"x"); change({value:1});',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(metadata,"x",{value:1});',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty,null); change([metadata,"x",{value:1}]);',
|
||||
'const change = Object.defineProperty.bind(null,Object,"create");',
|
||||
'Object.defineProperty.call(null,metadata,"x",{value:1});',
|
||||
'Object.defineProperty.apply(null,[metadata,"x",{value:1}]);',
|
||||
|
||||
@@ -43,6 +43,8 @@ export const electronMemberPath = (
|
||||
|
||||
/** A known intrinsic callable and its examined positional arguments. */
|
||||
export interface JavaScriptIntrinsicInvocation {
|
||||
/** The original bound receiver; rebinding cannot replace it. */
|
||||
readonly receiver?: t.Node;
|
||||
readonly path: readonly string[];
|
||||
readonly arguments:
|
||||
| readonly t.CallExpression["arguments"][number][]
|
||||
|
||||
@@ -5,11 +5,6 @@ import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
const arrayArguments = (array: t.Node | undefined) =>
|
||||
t.isArrayExpression(array) &&
|
||||
@@ -35,62 +30,79 @@ const invocationArguments = (
|
||||
*/
|
||||
export const resolveJavaScriptIntrinsicInvocation = (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
intrinsicPath: (node: t.Node) => readonly string[] | undefined,
|
||||
boundIntrinsic: (node: t.Node) => JavaScriptIntrinsicInvocation | undefined,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
const visited = new Set<string>();
|
||||
const visited = new Set<t.Node>();
|
||||
let callee: t.Node = node.callee;
|
||||
let arguments_: JavaScriptIntrinsicInvocation["arguments"] = node.arguments;
|
||||
let receiver: t.Node | undefined;
|
||||
for (;;) {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (visited.has(callee)) return undefined;
|
||||
visited.add(callee);
|
||||
let path = intrinsicPath(callee);
|
||||
// Static projections can capture call/apply themselves as aliases.
|
||||
while (path !== undefined && path.length > 3) {
|
||||
if (path === undefined) {
|
||||
if (
|
||||
t.isMemberExpression(callee) ||
|
||||
t.isOptionalMemberExpression(callee)
|
||||
) {
|
||||
const method = semanticStaticPropertyKey(
|
||||
callee.property,
|
||||
callee.computed,
|
||||
);
|
||||
if (method === "call" || method === "apply") {
|
||||
receiver = arguments_?.[0];
|
||||
callee = callee.object;
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bound = boundIntrinsic(callee);
|
||||
if (bound === undefined) return undefined;
|
||||
path = bound.path;
|
||||
receiver = bound.receiver;
|
||||
arguments_ =
|
||||
bound.arguments === undefined
|
||||
? undefined
|
||||
: [...bound.arguments, ...(arguments_ ?? [])];
|
||||
}
|
||||
// A projected .call/.apply invokes the preceding callable. The actual
|
||||
// Function.prototype methods instead invoke their receiver, retained by bind.
|
||||
while (path.length > 3 && !genericInvocationMethod(path)) {
|
||||
const method = path.at(-1);
|
||||
if (method !== "call" && method !== "apply") break;
|
||||
receiver = arguments_?.[0];
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
if (path?.join(".") === "intrinsic.Reflect.apply") {
|
||||
const genericMethod = genericInvocationMethod(path);
|
||||
if (genericMethod !== undefined) {
|
||||
if (!t.isExpression(receiver)) return undefined;
|
||||
callee = receiver;
|
||||
receiver = arguments_?.[0];
|
||||
arguments_ = invocationArguments(genericMethod, arguments_);
|
||||
continue;
|
||||
}
|
||||
if (path.join(".") === "intrinsic.Reflect.apply") {
|
||||
const callable = arguments_?.[0];
|
||||
if (!t.isExpression(callable)) return undefined;
|
||||
callee = callable;
|
||||
receiver = arguments_?.[1];
|
||||
arguments_ = arrayArguments(arguments_?.[2]);
|
||||
continue;
|
||||
}
|
||||
if (path !== undefined) return { path, arguments: arguments_ };
|
||||
if (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) {
|
||||
const method = semanticStaticPropertyKey(
|
||||
callee.property,
|
||||
callee.computed,
|
||||
);
|
||||
if (method === "call" || method === "apply") {
|
||||
callee = callee.object;
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bound = boundIntrinsic(callee);
|
||||
if (bound !== undefined)
|
||||
return {
|
||||
path: bound.path,
|
||||
arguments:
|
||||
bound.arguments === undefined
|
||||
? undefined
|
||||
: [...bound.arguments, ...(arguments_ ?? [])],
|
||||
};
|
||||
if (!t.isIdentifier(callee)) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, callee, callee.name);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
callee = initializer.node;
|
||||
return { path, arguments: arguments_ };
|
||||
}
|
||||
};
|
||||
|
||||
const genericInvocationMethod = (
|
||||
path: readonly string[],
|
||||
): "call" | "apply" | undefined =>
|
||||
path.length === 4 &&
|
||||
path[0] === "intrinsic" &&
|
||||
path[1] === "Function" &&
|
||||
path[2] === "prototype" &&
|
||||
(path[3] === "call" || path[3] === "apply")
|
||||
? path[3]
|
||||
: undefined;
|
||||
@@ -212,6 +212,7 @@ export const createSemanticModuleInterop = (
|
||||
const argumentGroups: t.CallExpression["arguments"][] = [];
|
||||
const bindReads: t.Node[] = [];
|
||||
let opaqueArguments = false;
|
||||
let receiver: t.Node | undefined;
|
||||
let current = node;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
@@ -243,11 +244,9 @@ export const createSemanticModuleInterop = (
|
||||
// intervening members would require a separate proof of their identity.
|
||||
if (!terminal && members.length !== 1) return undefined;
|
||||
bindReads.push(captured?.capturedAt ?? current.callee);
|
||||
const thisArgument = current.arguments[0];
|
||||
receiver = current.arguments[0];
|
||||
const ownArguments = current.arguments.slice(1);
|
||||
opaqueArguments ||=
|
||||
thisArgument !== undefined &&
|
||||
!readExactJavaScriptLiteral(thisArgument).found;
|
||||
opaqueArguments ||= t.isSpreadElement(receiver);
|
||||
opaqueArguments ||= ownArguments.some((argument) =>
|
||||
t.isSpreadElement(argument),
|
||||
);
|
||||
@@ -271,6 +270,7 @@ export const createSemanticModuleInterop = (
|
||||
});
|
||||
return {
|
||||
path,
|
||||
...(receiver === undefined ? {} : { receiver }),
|
||||
// Rebinding appends arguments to the earlier bound argument list.
|
||||
arguments: opaqueArguments
|
||||
? undefined
|
||||
@@ -549,12 +549,7 @@ export const createSemanticModuleInterop = (
|
||||
callOrigin,
|
||||
intrinsicPath,
|
||||
intrinsicInvocation: (node: t.CallExpression | t.OptionalCallExpression) =>
|
||||
resolveJavaScriptIntrinsicInvocation(
|
||||
node,
|
||||
state,
|
||||
intrinsicPath,
|
||||
boundIntrinsic,
|
||||
),
|
||||
resolveJavaScriptIntrinsicInvocation(node, intrinsicPath, boundIntrinsic),
|
||||
boundIntrinsic,
|
||||
staticString,
|
||||
isFreshObject,
|
||||
|
||||
@@ -10,6 +10,26 @@ import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const INTRINSIC_MUTATIONS = [
|
||||
["poisoned.cjs", "({Object} = replacements);"],
|
||||
[
|
||||
"projected.cjs",
|
||||
'const {defineProperty:change} = Object; change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"decorated.cjs",
|
||||
'@(() => Object) class metadata {}; Object.defineProperty(metadata,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"uncurried.cjs",
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"bound.cjs",
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||
],
|
||||
] as const;
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains converted Electron windows and preload evidence through %s",
|
||||
async (surface, { cli }) => {
|
||||
@@ -22,21 +42,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
join(root, "preload.js"),
|
||||
'throw new Error("must stay inert");',
|
||||
);
|
||||
for (const [name, mutation] of [
|
||||
["poisoned.cjs", "({Object} = replacements);"],
|
||||
[
|
||||
"projected.cjs",
|
||||
'const {defineProperty:change} = Object; change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"decorated.cjs",
|
||||
'@(() => Object) class metadata {}; Object.defineProperty(metadata,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"bound.cjs",
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||
],
|
||||
] as const)
|
||||
for (const [name, mutation] of INTRINSIC_MUTATIONS)
|
||||
await writeFile(
|
||||
join(root, name),
|
||||
`${mutation}
|
||||
@@ -86,6 +92,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
require("./projected.cjs");
|
||||
require("./decorated.cjs");
|
||||
require("./bound.cjs");
|
||||
require("./uncurried.cjs");
|
||||
throw new Error("must not execute main");
|
||||
`;
|
||||
await writeFile(join(root, "main.cjs"), source);
|
||||
@@ -112,7 +119,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
preload_entrypoints: 1,
|
||||
});
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
expect(result.statistics.parsed_javascript_files).toBe(6);
|
||||
expect(result.statistics.parsed_javascript_files).toBe(7);
|
||||
const windows = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "browser-window",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user