From 98540f24e0ed3e46139704eb5e1923f628a33aee Mon Sep 17 00:00:00 2001 From: gensokyo-dev Date: Sat, 14 Nov 2026 13:26:17 +0000 Subject: [PATCH] fix(javascript): stop an Electron alias walk at a read before its initializer The alias walk followed `var saved=s; var s=n(K)` to the later Electron load although `saved` captured undefined. Each hop now checks, as the consumer read already did, that the aliased binding's stable origin is available at that read. The walk also shares one visited set instead of copying it at every hop. Refs #4490 Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- .../electronBundledRequireIdentity.test.ts | 4 ++ .../javascript/javascriptSemanticAnalysis.ts | 38 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/domain/javascript/electronBundledRequireIdentity.test.ts b/src/domain/javascript/electronBundledRequireIdentity.test.ts index 390f967a4d..0e81e70506 100644 --- a/src/domain/javascript/electronBundledRequireIdentity.test.ts +++ b/src/domain/javascript/electronBundledRequireIdentity.test.ts @@ -703,6 +703,10 @@ describe("Electron members destructured from a Webpack external binding", () => "const s=n(45840);const w=s;w.BrowserWindow=class{};const{BrowserWindow}=s;", ], ["a cyclic alias", "var a=b,b=a;const{BrowserWindow}=a;"], + [ + "an alias read before its hoisted source is initialized", + "var saved=s;var s=n(45840);const{BrowserWindow}=saved;", + ], ["a different table key", "const s=n(45841);const{BrowserWindow}=s;"], [ "a rebindable binding exposed to direct eval", diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 182272200e..7f5d6339a7 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -882,17 +882,7 @@ export function* indexJavaScriptElectronIdentitySteps( const originAvailableAtRead = ( binding: JavaScriptSemanticBindingState, read: t.Node, - ): boolean => { - if ( - binding.directOrigins.length > 0 || - semanticStableInitializer(binding, state) === undefined - ) - return true; - const access = semanticBindingAtRead(binding, read, state); - // An unresolved activation can still carry a possible identity, while an - // ordered read before its initializer cannot refer to that value. - return access.status !== "ordered" || access.hasDefiniteInitializer; - }; + ): boolean => originAvailableAt(binding, read, state); const rootOrigin = (root: t.Node): readonly string[] | undefined => { if (t.isIdentifier(root)) { if (semanticResolutionBlocked(state, root, root.name)) return undefined; @@ -1139,16 +1129,35 @@ const electronRequireOrigin = ( semanticRequireOrigin(node, state) ?? semanticBundledRequireOrigin(node, state); +/** Whether a read can see the binding's stable origin rather than an earlier value. */ +const originAvailableAt = ( + binding: JavaScriptSemanticBindingState, + read: t.Node, + state: JavaScriptSemanticAnalysisState, +): boolean => { + if ( + binding.directOrigins.length > 0 || + semanticStableInitializer(binding, state) === undefined + ) + return true; + const access = semanticBindingAtRead(binding, read, state); + // An unresolved activation can still carry a possible identity, while an + // ordered read before its initializer cannot refer to that value. + return access.status !== "ordered" || access.hasDefiniteInitializer; +}; + /** * 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. + * stops at a rebindable binding that direct eval may have changed, and at an + * alias read that runs before the aliased binding's initializer. The chain has + * one alias per hop, so one visited set serves the whole walk. */ const bindingOrigin = ( binding: JavaScriptSemanticBindingState, state: JavaScriptSemanticAnalysisState, - visited: ReadonlySet = new Set(), + visited: Set = new Set(), ): JavaScriptModuleOrigin | undefined => { if (binding.directOrigins.length > 0) return binding.directOrigins.length === 1 @@ -1172,13 +1181,14 @@ const bindingOrigin = ( ? electronRequireOrigin(initializer.node, state)?.origin : visited.has(alias.bindingId) || exhausted || + !originAvailableAt(alias, source, state) || [binding, alias].some( (hop) => semanticBindingCanBeRebound(hop) && semanticBindingMayHaveOpaqueEffect(hop, state), ) ? undefined - : bindingOrigin(alias, state, new Set([...visited, binding.bindingId])); + : bindingOrigin(alias, state, visited.add(binding.bindingId)); const projection = initializer.projection; return origin === undefined || !projection.every((key): key is string => typeof key === "string")