fix(javascript): follow Electron through aliases of Webpack externals

General provenance only knew a literal require, so a bundled Electron
external held in one binding and destructured from another
(`const s=n(K);const{BrowserWindow}=s`) lost its windows and IPC. The
Electron binding origin now follows stable lexical aliases, and stops at
a rebindable binding that direct eval may have changed.

Fixes #4490
Supersedes #6993

Squashed from #6993 and rebased onto main. Rebase adaptation: the walk
uses the caller-selectable precision.alias_selections budget (the fixed
SEMANTIC_ALIAS_SELECTION_LIMIT it used was removed by #9706) and records
alias-selections when it stops there; a test pins that an over-budget
chain is not claimed and the limit is reported.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
mohammadasim224andgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:44:47 +01:00
1 parent 3a25046b8c
commit f99d73bf39
3 files changed
+131 -1

No files matched your search

@@ -316,6 +316,27 @@ describe("Webpack loader alias bounds", () => {
});
});
it("reports the alias budget when a destructured external alias chain exceeds it", () => {
// 520 const hops exceed the default 512 alias selections; the origin is not
// claimed, and the walk reports its limit instead of a silent absence.
const hops = Array.from(
{ length: 520 },
(_, index) => `const a${index + 1}=a${index};`,
).join("");
const analysis = analyzeJavaScriptStaticSource(
bootstrap(
EXTERNAL,
`const a0=n(45840);${hops}const{BrowserWindow}=a520;new BrowserWindow({});`,
),
);
expect(analysis.electron.browser_windows).toEqual([]);
expect(analysis.analysis_coverage).toMatchObject({
examination: {
semantic_resource_limits: expect.arrayContaining(["alias-selections"]),
},
});
});
describe("Webpack externals with a tampered runtime", () => {
it.each([
[
@@ -632,3 +653,74 @@ describe("Webpack identity through erased TypeScript syntax", () => {
},
);
});
describe("Electron members destructured from a Webpack external binding", () => {
const WINDOW = 'webPreferences:{preload:"./p.js",nodeIntegration:true}';
const directIpc = (source: string) =>
electronOf(source).ipc.filter(
({ resolution_basis: basis }) => basis !== "wrapper-substitution",
);
it.each([
["a shorthand pattern", "const{BrowserWindow}=s;new BrowserWindow"],
["a renamed pattern", "const{BrowserWindow:W}=s;new W"],
["a two-hop alias", "const w=s;const{BrowserWindow}=w;new BrowserWindow"],
["a plain alias", "const w=s;new w.BrowserWindow"],
])("follows the window class through %s", (_name, consumer) => {
expect(
electronOf(
bootstrap(EXTERNAL, `const s=n(45840);${consumer}({${WINDOW}});`),
).browser_windows,
).toMatchObject([{ preload_path: "./p.js" }]);
});
it.each([
[
"a top-level registration",
'const s=n(45840);const{app,ipcMain}=s;ipcMain.handle("chan",async()=>[]);',
],
[
"a guarded registration in a nested function",
'const s=n(45840);const{app,ipcMain}=s;const init=()=>{if(!ipcMain||!app)return;ipcMain.on("chan",()=>{})};init();',
],
])("follows destructured IPC through %s", (_name, consumer) => {
expect(directIpc(bootstrap(EXTERNAL, consumer))).toMatchObject([
{ channel: "chan", side: "main" },
]);
});
it.each([
[
"a reassigned external binding",
"let s=n(45840);s=other;const{BrowserWindow}=s;",
],
[
"an overwritten class member",
"const s=n(45840);s.BrowserWindow=class{};const{BrowserWindow}=s;",
],
[
"an overwritten member through an alias",
"const s=n(45840);const w=s;w.BrowserWindow=class{};const{BrowserWindow}=s;",
],
["a cyclic alias", "var a=b,b=a;const{BrowserWindow}=a;"],
["a different table key", "const s=n(45841);const{BrowserWindow}=s;"],
[
"a rebindable binding exposed to direct eval",
"let s=n(45840);eval(code);const{BrowserWindow}=s;",
],
])("keeps %s unresolved", (_name, prefix) => {
expect(
electronOf(
bootstrap(EXTERNAL, `${prefix}new BrowserWindow({${WINDOW}});`),
).browser_windows,
).toEqual([]);
});
it("keeps a require alias exposed to direct eval unresolved", () => {
expect(
electronOf(
`let s=require("electron");eval(code);const{BrowserWindow}=s;new BrowserWindow({${WINDOW}});`,
).browser_windows,
).toEqual([]);
});
});
@@ -9,6 +9,7 @@ import {
semanticOpaqueBindingEffects,
semanticBindingCanBeRebound,
semanticOpaqueBindingReadMayHaveChanged,
semanticBindingMayHaveOpaqueEffect,
} from "./javascriptSemanticOpaqueEffects.js";
import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js";
import * as t from "@babel/types";
@@ -1138,9 +1139,16 @@ const electronRequireOrigin = (
semanticRequireOrigin(node, state) ??
semanticBundledRequireOrigin(node, state);
/**
* General provenance only knows literal `require`, so a bundled external held
* in one binding and destructured from another (`const s=n(K);const{X}=s`)
* is followed here through stable lexical aliases. Like provenance, a hop
* stops at a rebindable binding that direct eval may have changed.
*/
const bindingOrigin = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
visited: ReadonlySet<string> = new Set(),
): JavaScriptModuleOrigin | undefined => {
if (binding.directOrigins.length > 0)
return binding.directOrigins.length === 1
@@ -1148,7 +1156,29 @@ const bindingOrigin = (
: undefined;
const initializer = semanticStableInitializer(binding, state);
if (initializer === undefined) return undefined;
const origin = electronRequireOrigin(initializer.node, state)?.origin;
const source = unwrapJavaScriptExpression(initializer.node).node;
const alias =
t.isIdentifier(source) &&
!semanticResolutionBlocked(state, source, source.name)
? resolveSemanticBindingState(state, source, source.name)
: undefined;
// The alias walk shares the caller-selectable alias budget; running out is a
// reported resource limit, not a silent absence of the origin.
const exhausted =
alias !== undefined && visited.size >= state.precision.alias_selections;
if (exhausted) state.valueResourceLimits.add("alias-selections");
const origin =
alias === undefined
? electronRequireOrigin(initializer.node, state)?.origin
: visited.has(alias.bindingId) ||
exhausted ||
[binding, alias].some(
(hop) =>
semanticBindingCanBeRebound(hop) &&
semanticBindingMayHaveOpaqueEffect(hop, state),
)
? undefined
: bindingOrigin(alias, state, new Set([...visited, binding.bindingId]));
const projection = initializer.projection;
return origin === undefined ||
!projection.every((key): key is string => typeof key === "string")