mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 18:35:53 +00:00
The canonical `n.n` getter proof scanned for direct `loader.n =` assignments and four Object methods on its own. `Reflect.set`, `Reflect.defineProperty`, `delete`, destructuring and unknown-key writes left the proof intact, so a replaced or deleted getter still attributed BrowserWindow calls to Electron. Any Object method call on the loader also rejected the proof, even for an unrelated key. The proof now uses the shared identity-write owner (`collectJavaScriptIdentityWritesSteps` with the module interop's intrinsic resolution), as ambient writes already do. A write whose path may select `n` (the key itself, an unknown key, or the whole loader) counts; exactly one canonical assignment installed before entry dispatch remains required. Fixes #3315 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
|
|
import { proveJavaScriptBootstrapIntegrity } from "./javascriptBootstrapIntegrity.js";
|
|
import {
|
|
semanticStaticPropertyKey,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js";
|
|
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
|
import {
|
|
semanticMemberChain,
|
|
semanticRequireOrigin,
|
|
type JavaScriptRequireOrigin,
|
|
} from "./javascriptSemanticRequireOrigin.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticResolutionBlocked,
|
|
type JavaScriptSemanticAnalysisState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
argumentValue,
|
|
moduleFactory,
|
|
modulePropertyName,
|
|
} from "./javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
completeSemanticSteps,
|
|
traverseJavaScriptAst,
|
|
} from "./javascriptSemanticTraversal.js";
|
|
|
|
/** Loader bindings and literal externals proven for one standalone bootstrap. */
|
|
interface BootstrapExternals {
|
|
/** The runtime loader and every factory parameter it is passed as. */
|
|
readonly loaderBindingIds: ReadonlySet<string>;
|
|
/** Loader bindings with the canonical n.n getter installed before dispatch. */
|
|
readonly interopGetterBindingIds: ReadonlySet<string>;
|
|
/** Table keys whose factory only re-exports one unshadowed literal require; null when a key repeats. */
|
|
readonly externals: ReadonlyMap<string, string | null>;
|
|
}
|
|
|
|
const proofs = new WeakMap<
|
|
JavaScriptSemanticAnalysisState,
|
|
WeakMap<t.CallExpression, BootstrapExternals | null>
|
|
>();
|
|
|
|
/**
|
|
* Recover a literal require reached through a proven Webpack bootstrap.
|
|
*
|
|
* Webpack emits externals as `{45840: e => { e.exports = require("electron") }}`
|
|
* and consumers call the runtime loader, `n(45840)`, instead of `require`. The
|
|
* origin is accepted only when the loader binding is the bootstrap's own loader
|
|
* or a factory parameter that the bootstrap proves it passes the loader as, and
|
|
* the keyed factory consists solely of that re-export.
|
|
*/
|
|
export const semanticBundledRequireOrigin = (
|
|
node: t.Node | null | undefined,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): JavaScriptRequireOrigin | undefined => {
|
|
const chain = semanticMemberChain(node);
|
|
const current = chain?.root;
|
|
if (
|
|
chain?.members.length === 0 &&
|
|
t.isCallExpression(current) &&
|
|
t.isMemberExpression(current.callee) &&
|
|
semanticStaticPropertyKey(
|
|
current.callee.property,
|
|
current.callee.computed,
|
|
) === "n" &&
|
|
t.isIdentifier(current.callee.object) &&
|
|
current.arguments.length === 1
|
|
) {
|
|
// Webpack's `n.n(module)` returns its default-export getter. For a
|
|
// CommonJS external, invoking that getter yields the exact exports object;
|
|
// bind its identity to the proven registry result while retaining the
|
|
// nested loader call as the source of provenance.
|
|
const nested = semanticBundledRequireOrigin(current.arguments[0], state);
|
|
const loaderBinding = resolveSemanticBindingState(
|
|
state,
|
|
current.callee.object,
|
|
current.callee.object.name,
|
|
);
|
|
const proof =
|
|
nested === undefined || loaderBinding === undefined
|
|
? undefined
|
|
: bootstrapProofForLoaderCall(
|
|
nested.call,
|
|
loaderBinding.bindingId,
|
|
state,
|
|
);
|
|
const nestedCallee = nested?.call.callee;
|
|
const loaderBindingId = loaderBinding?.bindingId;
|
|
if (
|
|
nested !== undefined &&
|
|
nested.origin.importedPath.length === 0 &&
|
|
t.isIdentifier(nestedCallee) &&
|
|
loaderBindingId !== undefined &&
|
|
loaderBindingId ===
|
|
resolveSemanticBindingState(state, nestedCallee, nestedCallee.name)
|
|
?.bindingId &&
|
|
proof?.loaderBindingIds.has(loaderBindingId) === true &&
|
|
proof.interopGetterBindingIds.has(loaderBindingId)
|
|
)
|
|
return nested;
|
|
}
|
|
if (
|
|
chain === undefined ||
|
|
!t.isCallExpression(current) ||
|
|
!t.isIdentifier(current.callee) ||
|
|
current.arguments.length !== 1
|
|
)
|
|
return undefined;
|
|
const key = argumentValue(current.arguments[0]);
|
|
if (key === undefined) return undefined;
|
|
const loader = current.callee;
|
|
if (semanticResolutionBlocked(state, loader, loader.name)) return undefined;
|
|
const binding = resolveSemanticBindingState(state, loader, loader.name);
|
|
if (
|
|
binding === undefined ||
|
|
binding.definitions.length !== 1 ||
|
|
binding.definitions[0]?.kind === "assignment"
|
|
)
|
|
return undefined;
|
|
for (
|
|
let ancestor = state.parentsByNode.get(current);
|
|
ancestor !== undefined;
|
|
ancestor = state.parentsByNode.get(ancestor)
|
|
) {
|
|
if (!t.isCallExpression(ancestor)) continue;
|
|
const proof = bootstrapExternals(ancestor, state);
|
|
if (proof === null || !proof.loaderBindingIds.has(binding.bindingId))
|
|
continue;
|
|
const specifier = proof.externals.get(key);
|
|
return typeof specifier === "string"
|
|
? {
|
|
origin: {
|
|
specifier,
|
|
importedPath: chain.members,
|
|
resolutionMode: "require",
|
|
},
|
|
call: current,
|
|
}
|
|
: undefined;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/** Prove the source-local n.n getter and reject visible replacements. */
|
|
const canonicalWebpackInteropBindings = (
|
|
loaderBindingIds: ReadonlySet<string>,
|
|
bootstrapBody: t.BlockStatement,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): ReadonlySet<string> => {
|
|
// The shared identity-write owner covers assignment, delete, destructuring,
|
|
// and Object/Reflect mutators; any write that may select `n` counts.
|
|
const writes = new Set<t.Node>();
|
|
const interop = createSemanticModuleInterop(state);
|
|
completeSemanticSteps(
|
|
collectJavaScriptIdentityWritesSteps(
|
|
state.program,
|
|
(root, kind) => {
|
|
const node = unwrapJavaScriptExpression(root).node;
|
|
return kind === "member" &&
|
|
t.isIdentifier(node) &&
|
|
loaderBindingIds.has(
|
|
resolveSemanticBindingState(state, node, node.name)?.bindingId ??
|
|
"",
|
|
)
|
|
? [["loader"]]
|
|
: [];
|
|
},
|
|
interop.intrinsicInvocation,
|
|
interop.staticString,
|
|
interop.isFreshObject,
|
|
(node, path) => {
|
|
if (path.length < 2 || path[1] === "n" || path[1] === null)
|
|
writes.add(node);
|
|
},
|
|
),
|
|
);
|
|
const proven = new Set<string>();
|
|
const [write] = writes;
|
|
if (
|
|
writes.size === 1 &&
|
|
t.isAssignmentExpression(write) &&
|
|
isInstalledBeforeEntryDispatch(
|
|
write,
|
|
bootstrapBody,
|
|
loaderBindingIds,
|
|
state,
|
|
) &&
|
|
isCanonicalGetter(write, state)
|
|
)
|
|
for (const bindingId of loaderBindingIds) proven.add(bindingId);
|
|
return proven;
|
|
};
|
|
|
|
const isCanonicalGetter = (
|
|
write: t.AssignmentExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
if (
|
|
!t.isArrowFunctionExpression(write.right) ||
|
|
write.right.params.length !== 1 ||
|
|
!t.isIdentifier(write.right.params[0])
|
|
)
|
|
return false;
|
|
const parameter = write.right.params[0];
|
|
const parameterBinding = resolveSemanticBindingState(
|
|
state,
|
|
parameter,
|
|
parameter.name,
|
|
);
|
|
const body = write.right.body;
|
|
return (
|
|
parameterBinding !== undefined &&
|
|
t.isConditionalExpression(body) &&
|
|
t.isLogicalExpression(body.test, { operator: "&&" }) &&
|
|
sameBinding(body.test.left, parameterBinding.bindingId, state) &&
|
|
t.isMemberExpression(body.test.right) &&
|
|
semanticStaticPropertyKey(
|
|
body.test.right.property,
|
|
body.test.right.computed,
|
|
) === "__esModule" &&
|
|
sameBinding(body.test.right.object, parameterBinding.bindingId, state) &&
|
|
getterReturnsDefault(body.consequent, parameterBinding.bindingId, state) &&
|
|
getterReturnsModule(body.alternate, parameterBinding.bindingId, state)
|
|
);
|
|
};
|
|
|
|
const isInstalledBeforeEntryDispatch = (
|
|
write: t.AssignmentExpression,
|
|
body: t.BlockStatement,
|
|
loaderBindingIds: ReadonlySet<string>,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
const statement = state.parentsByNode.get(write);
|
|
if (!t.isExpressionStatement(statement)) return false;
|
|
const writeIndex = body.body.indexOf(statement);
|
|
if (writeIndex < 0) return false;
|
|
let entryIndex = -1;
|
|
for (let index = 0; index < body.body.length; index += 1) {
|
|
const topLevel = body.body[index];
|
|
if (topLevel === undefined) continue;
|
|
traverseJavaScriptAst(topLevel, {
|
|
enter: (node, _parent, ancestors) => {
|
|
if (
|
|
entryIndex < 0 &&
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee) &&
|
|
ancestors().every((ancestor) => !t.isFunction(ancestor)) &&
|
|
loaderBindingIds.has(
|
|
resolveSemanticBindingState(state, node.callee, node.callee.name)
|
|
?.bindingId ?? "",
|
|
)
|
|
)
|
|
entryIndex = index;
|
|
},
|
|
});
|
|
if (entryIndex >= 0) break;
|
|
}
|
|
return entryIndex > writeIndex;
|
|
};
|
|
|
|
const bootstrapProofForLoaderCall = (
|
|
call: t.CallExpression,
|
|
loaderBindingId: string,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): BootstrapExternals | undefined => {
|
|
for (
|
|
let ancestor = state.parentsByNode.get(call);
|
|
ancestor !== undefined;
|
|
ancestor = state.parentsByNode.get(ancestor)
|
|
) {
|
|
if (!t.isCallExpression(ancestor)) continue;
|
|
const proof = bootstrapExternals(ancestor, state);
|
|
if (proof?.loaderBindingIds.has(loaderBindingId)) return proof;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const sameBinding = (
|
|
node: t.Node,
|
|
bindingId: string,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
t.isIdentifier(node) &&
|
|
resolveSemanticBindingState(state, node, node.name)?.bindingId === bindingId;
|
|
|
|
const getterReturnsDefault = (
|
|
node: t.Node,
|
|
bindingId: string,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
t.isArrowFunctionExpression(node) &&
|
|
node.params.length === 0 &&
|
|
t.isMemberExpression(node.body) &&
|
|
semanticStaticPropertyKey(node.body.property, node.body.computed) ===
|
|
"default" &&
|
|
sameBinding(node.body.object, bindingId, state);
|
|
|
|
const getterReturnsModule = (
|
|
node: t.Node,
|
|
bindingId: string,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
t.isArrowFunctionExpression(node) &&
|
|
node.params.length === 0 &&
|
|
sameBinding(node.body, bindingId, state);
|
|
|
|
const bootstrapExternals = (
|
|
call: t.CallExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): BootstrapExternals | null => {
|
|
let byCall = proofs.get(state);
|
|
if (byCall === undefined) {
|
|
byCall = new WeakMap();
|
|
proofs.set(state, byCall);
|
|
}
|
|
const cached = byCall.get(call);
|
|
if (cached !== undefined) return cached;
|
|
const proof = proveBootstrapExternals(call, state);
|
|
byCall.set(call, proof);
|
|
return proof;
|
|
};
|
|
|
|
const proveBootstrapExternals = (
|
|
call: t.CallExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): BootstrapExternals | null => {
|
|
const bootstrap = javascriptBootstrapAt(call, state);
|
|
// A replaced loader no longer dispatches the declared table.
|
|
if (
|
|
bootstrap === undefined ||
|
|
!singleDefinition(state, bootstrap.loaderBindingId, "function")
|
|
)
|
|
return null;
|
|
const loaderBindingIds = new Set([bootstrap.loaderBindingId]);
|
|
const factories = bootstrap.table.properties.flatMap((property) => {
|
|
const factory = moduleFactory(property);
|
|
return factory === undefined ? [] : [{ property, factory }];
|
|
});
|
|
for (const { factory } of factories) {
|
|
// The bootstrap proves `table[key](module, module.exports, loader)`.
|
|
const loaderParameter = factory.params[2];
|
|
const bindingId = t.isIdentifier(loaderParameter)
|
|
? resolveSemanticBindingState(
|
|
state,
|
|
loaderParameter,
|
|
loaderParameter.name,
|
|
)?.bindingId
|
|
: undefined;
|
|
if (
|
|
bindingId !== undefined &&
|
|
singleDefinition(state, bindingId, "parameter")
|
|
)
|
|
loaderBindingIds.add(bindingId);
|
|
}
|
|
const integrity = proveJavaScriptBootstrapIntegrity(
|
|
call,
|
|
bootstrap,
|
|
loaderBindingIds,
|
|
state,
|
|
);
|
|
if (integrity === undefined) return null;
|
|
const externals = new Map<string, string | null>();
|
|
for (const { property, factory } of factories) {
|
|
const key = modulePropertyName(property);
|
|
const specifier = integrity.replacedKeys.has(key)
|
|
? undefined
|
|
: externalSpecifier(factory, state);
|
|
if (externals.has(key)) externals.set(key, null);
|
|
else externals.set(key, specifier ?? null);
|
|
}
|
|
return {
|
|
loaderBindingIds,
|
|
interopGetterBindingIds: canonicalWebpackInteropBindings(
|
|
loaderBindingIds,
|
|
bootstrap.body,
|
|
state,
|
|
),
|
|
externals,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Accept only `module.exports = require("specifier")` as the whole body of an
|
|
* ordinary factory; generator, async and accessor factories do not run it.
|
|
*/
|
|
const externalSpecifier = (
|
|
factory: t.FunctionExpression | t.ArrowFunctionExpression | t.ObjectMethod,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): string | undefined => {
|
|
const moduleParameter = factory.params[0];
|
|
const names = factory.params.map((parameter) =>
|
|
t.isIdentifier(parameter) ? parameter.name : null,
|
|
);
|
|
if (
|
|
!t.isIdentifier(moduleParameter) ||
|
|
// A duplicated name binds the later argument, not the module object.
|
|
new Set(names).size !== names.length ||
|
|
factory.generator ||
|
|
factory.async ||
|
|
(t.isObjectMethod(factory) && factory.kind !== "method")
|
|
)
|
|
return undefined;
|
|
let expression: t.Node | undefined;
|
|
if (t.isBlockStatement(factory.body)) {
|
|
const [statement] = factory.body.body;
|
|
if (factory.body.body.length !== 1 || !t.isExpressionStatement(statement))
|
|
return undefined;
|
|
expression = statement.expression;
|
|
} else expression = factory.body;
|
|
if (
|
|
!t.isAssignmentExpression(expression, { operator: "=" }) ||
|
|
!t.isMemberExpression(expression.left) ||
|
|
!t.isIdentifier(expression.left.object) ||
|
|
semanticStaticPropertyKey(
|
|
expression.left.property,
|
|
expression.left.computed,
|
|
) !== "exports"
|
|
)
|
|
return undefined;
|
|
const moduleBinding = resolveSemanticBindingState(
|
|
state,
|
|
moduleParameter,
|
|
moduleParameter.name,
|
|
);
|
|
const target = resolveSemanticBindingState(
|
|
state,
|
|
expression.left.object,
|
|
expression.left.object.name,
|
|
);
|
|
if (
|
|
moduleBinding === undefined ||
|
|
target?.bindingId !== moduleBinding.bindingId
|
|
)
|
|
return undefined;
|
|
const required = semanticRequireOrigin(expression.right, state)?.origin;
|
|
return required !== undefined && required.importedPath.length === 0
|
|
? required.specifier
|
|
: undefined;
|
|
};
|
|
|
|
const singleDefinition = (
|
|
state: JavaScriptSemanticAnalysisState,
|
|
bindingId: string,
|
|
kind: "function" | "parameter",
|
|
): boolean => {
|
|
const definitions = state.bindingsById.get(bindingId)?.definitions;
|
|
return definitions?.length === 1 && definitions[0]?.kind === kind;
|
|
};
|