Files
reverse-engineering-anything/src/domain/javascript/javascriptBundledRequireOrigin.ts
T

477 lines
15 KiB
TypeScript

import * as t from "@babel/types";
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
import { proveJavaScriptBootstrapIntegrity } from "./javascriptBootstrapIntegrity.js";
import {
semanticMemberChain,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
import {
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 loaderMayOwnReference = (root: t.Node): boolean => {
const pending = [root];
const seen = new Set<t.Node>();
while (pending.length > 0) {
const reference = pending.pop();
if (reference === undefined || seen.has(reference)) continue;
seen.add(reference);
if (seen.size > SEMANTIC_ALIAS_SELECTION_LIMIT) {
state.valueResourceLimits.add("alias-selections");
return true;
}
const node = unwrapJavaScriptExpression(reference).node;
if (!t.isIdentifier(node)) continue;
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) continue;
if (loaderBindingIds.has(binding.bindingId)) return true;
const selected = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
for (const source of [
...selected.initializers,
...selected.referenceInitializers,
])
if (source.projection.length === 0) pending.push(source.node);
}
return false;
};
const interop = createSemanticModuleInterop(state);
completeSemanticSteps(
collectJavaScriptIdentityWritesSteps(
state.program,
(root, kind) => {
return kind === "member" && loaderMayOwnReference(root)
? [["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;
};