mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
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:
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")
|
||||
|
||||
Reference in new issue
Block a user