[GH-ISSUE #4229] [Bug] Loader-alias getter replacement leaves false Electron BrowserWindow evidence #1660

Closed
opened 2026-10-16 21:08:41 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4229

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze-javascript-application reports an Electron BrowserWindow after a stable lexical alias of the Webpack loader replaces its canonical default-export getter with a getter returning a local class. The equivalent direct assignment and direct Reflect.set replacement correctly produce zero Electron BrowserWindows. All five analysis calls succeed. The alias result adds a browser-window graph node with authority: "ast-static-analysis", state: "observed", confidence: "exact", and complete observation coverage.

The replacement executes unconditionally before the only entry dispatch. The module table is unchanged. This is a remaining loader-alias mutation case after the historical getter-invalidation fix.

Reproduction

Use either tested source revision below, install its locked dependencies with npm ci, and build with npm run build:cached. Run this block from the repository root with the pinned Node on PATH. It creates disposable JavaScript directories, isolates configuration, invokes the real compiled CLI sequentially, and removes the fixtures. REA reads the sources as inert text; no Electron installation is required. The script was executed as written at both tested revisions.

node --input-type=module <<'JS'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const work = mkdtempSync(join(tmpdir(), 'getter-alias-'));
const home = join(work, 'home');
const config = join(work, 'config');
const cache = join(work, 'cache');
for (const directory of [home, config, cache]) mkdirSync(directory);
const prefix = `(() => {
  const table = {
    0: module => { module.exports = require('electron'); },
    1: (module, exports, load) => {
      const api = load.n(load(0));
      new (api().BrowserWindow)({});
    }
  };
  function load(key) {
    const module = {exports: {}};
    table[key](module, module.exports, load);
    return module.exports;
  }
  load.n = mod => mod && mod.__esModule ? () => mod.default : () => mod;
`;
const cases = {
  intact: '',
  direct: 'load.n = mod => () => ({BrowserWindow: class {}});',
  alias: 'const copy = load; copy.n = mod => () => ({BrowserWindow: class {}});',
  unrelated: 'const copy = load; copy.d = 1;',
  reflective: 'Reflect.set(load, "n", mod => () => ({BrowserWindow: class {}}));',
};
try {
  for (const [variant, mutation] of Object.entries(cases)) {
    const directory = join(work, variant);
    mkdirSync(directory);
    writeFileSync(join(directory, 'bundle.js'), prefix + mutation + '\nload(1);\n})();\n');
    const run = spawnSync(process.execPath, [
      'scripts/rea.mjs', 'analyze-javascript-application', directory,
      '--format', 'json', '--full-output',
    ], {
      encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024,
      env: {...process.env, HOME: home, XDG_CONFIG_HOME: config,
        XDG_CACHE_HOME: cache, npm_config_cache: cache},
    });
    if (run.status !== 0) throw Error(JSON.stringify({variant,
      status: run.status, error: run.error?.message, stderr: run.stderr}));
    const envelope = JSON.parse(run.stdout);
    if (envelope.ok !== true) throw Error(JSON.stringify(envelope));
    const value = envelope.data.normalized_result;
    const nodes = value.graph.nodes.filter(node => node.kind === 'browser-window');
    console.log(JSON.stringify({variant, exit: run.status,
      browser_windows: value.summary.browser_windows, window_nodes: nodes.length}));
  }
} finally {
  rmSync(work, {recursive: true, force: true});
}
JS

Expected behavior

The alias replacement should produce zero Electron BrowserWindow observations through the replaced getter, matching the direct and reflective replacement controls. The intact getter and unrelated alias-property write should each retain one. The genuine literal require('electron') remains a source observation in every fixture; invalidating the getter identity must preserve unrelated artifact and source facts.

The Electron identity rules require proven lexical aliases, leave visibly overwritten namespace members unresolved, and check direct assignment targets through aliases while preserving unrelated member writes. The canonical getter proof requires the canonical installation and rejects visible replacements. The existing getter tests cover intact getters, several direct-root replacement forms, and an unrelated-key control. Here copy and load refer to the same function object, so the literal local copy.n assignment replaces load.n before use without requiring arbitrary runtime reachability analysis.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A successful application analysis adds a false exact Electron BrowserWindow inventory entry and graph observation for a local replacement class. The demonstrated failure is bounded to a recognized bootstrap whose canonical getter is replaced through a loader alias; shipped-application prevalence and downstream analyst decisions were not measured.
  • Workaround status: unverified
  • Workaround basis: No caller option that preserves the original artifact and returns a corrected inventory was verified. Rewriting the alias assignment into a direct assignment changes the analyzed input and serves only as a control; manual source inspection does not correct the returned inventory.

REA version

Reproduced from source baseline 62bf09809ef7bb224028916424f8f07652a20f4f and independently replayed after rebuilding upstream main at d9aa06d93d95aa237927b83a433b495133302ef9. A source-only freshness recheck of true upstream main e2db768012079b82e6f1ff7191d475494b06d2a2 found no changes in the JavaScript analysis, query, public-adapter or cited contract paths compared with the replayed revision. That newer revision was inspected, not executed. The source manifest reports 6.5.0; these are repository builds, not an installed-release reproduction. Published-package applicability is NOT_ENOUGH_INFO because no installed release run or complete release-applicability proof was performed.

Runtime and platform

Repository-pinned Node v24.18.0 and npm 11.16.0 on macOS arm64, with locked dependencies retained. The current-main build completed all three preparation tasks successfully. No browser, external service, or native analysis engine is needed.

Target and artifact context

Local directory fixtures containing the complete reduced Webpack-style bootstrap above. The replacement getter returns a harmless local class. REA does not execute the fixtures. A separate JavaScript oracle executes only these generated sources with a counting Electron stub, not a real Electron engine.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Both source revisions produced:

{"variant":"intact","exit":0,"browser_windows":1,"window_nodes":1}
{"variant":"direct","exit":0,"browser_windows":0,"window_nodes":0}
{"variant":"alias","exit":0,"browser_windows":1,"window_nodes":1}
{"variant":"unrelated","exit":0,"browser_windows":1,"window_nodes":1}
{"variant":"reflective","exit":0,"browser_windows":0,"window_nodes":0}

The current-main alias graph was separately read back and retains the exact, observed Electron boundary evidence described above. The independent harmless runtime oracle completed all five fixture variants without an exception. It constructed the Electron stub once for intact and unrelated, and zero times for direct, alias and reflective replacements. The successful alias program therefore constructs a local class while REA still inventories an Electron BrowserWindow. This runtime oracle does not establish real Electron engine coverage.

Separate baseline verification passed all 41 existing bundled-require identity tests. Two temporary regression assertions requiring zero windows for alias replacements failed with actual length one, while their controls passed. Those temporary probes were removed; no source fix or passing restoration run is claimed.

  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

Run all five unchanged fixtures through the public workflow. The alias fixture must return zero Electron BrowserWindows through its replaced getter, matching direct and reflective. Preserve one for intact and unrelated, successful application analysis, and unrelated source observations. Check both graph observations and summary counts so stale exact attribution cannot survive behind a corrected count. The implementation and regression-test design remain open.

Analysis or suspected cause

Source-derived explanation: the getter proof's write-origin callback accepts only identifiers whose own binding IDs belong to the loader binding set. The lexical binding resolver resolves copy to its distinct binding without following its initializer. The shared write collector delegates member receiver identity to that callback. The alias write is therefore absent from the getter's write inventory, leaving the old canonical proof intact. The public analysis succeeds through normal input, artifact, graph and Evidence validation; this is a retained observation in a completed result.

Additional context

Closed #3315 was fixed by merged #3860. Its original direct-root Reflect.set failure is restored at the tested revisions, as the reflective control above confirms. Its executed restoration matrix and added regression cases do not include this stable loader-alias receiver. This is a residual alias-specific case, not a claim that the historical fix is absent.

Open #3883 replaces a computed module-table slot and uses no default getter. This fixture leaves the table unchanged and replaces only the getter through copy.n, so fixing that slot proof alone cannot restore this case.

No MCP, installed-package, other-platform, real-Electron, full-suite or post-fix restoration run was performed. Shipped-application prevalence and the last known good revision are NOT_ENOUGH_INFO because no corpus frequency or regression boundary was measured. A source rewrite into a direct assignment is only a discriminating control; no workaround preserving the original artifact and correcting its inventory was verified.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4229 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction ## Observed behavior `analyze-javascript-application` reports an Electron BrowserWindow after a stable lexical alias of the Webpack loader replaces its canonical default-export getter with a getter returning a local class. The equivalent direct assignment and direct `Reflect.set` replacement correctly produce zero Electron BrowserWindows. All five analysis calls succeed. The alias result adds a `browser-window` graph node with `authority: "ast-static-analysis"`, `state: "observed"`, `confidence: "exact"`, and complete observation coverage. The replacement executes unconditionally before the only entry dispatch. The module table is unchanged. This is a remaining loader-alias mutation case after the historical getter-invalidation fix. ## Reproduction Use either tested source revision below, install its locked dependencies with `npm ci`, and build with `npm run build:cached`. Run this block from the repository root with the pinned Node on `PATH`. It creates disposable JavaScript directories, isolates configuration, invokes the real compiled CLI sequentially, and removes the fixtures. REA reads the sources as inert text; no Electron installation is required. The script was executed as written at both tested revisions. ```sh node --input-type=module <<'JS' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; const work = mkdtempSync(join(tmpdir(), 'getter-alias-')); const home = join(work, 'home'); const config = join(work, 'config'); const cache = join(work, 'cache'); for (const directory of [home, config, cache]) mkdirSync(directory); const prefix = `(() => { const table = { 0: module => { module.exports = require('electron'); }, 1: (module, exports, load) => { const api = load.n(load(0)); new (api().BrowserWindow)({}); } }; function load(key) { const module = {exports: {}}; table[key](module, module.exports, load); return module.exports; } load.n = mod => mod && mod.__esModule ? () => mod.default : () => mod; `; const cases = { intact: '', direct: 'load.n = mod => () => ({BrowserWindow: class {}});', alias: 'const copy = load; copy.n = mod => () => ({BrowserWindow: class {}});', unrelated: 'const copy = load; copy.d = 1;', reflective: 'Reflect.set(load, "n", mod => () => ({BrowserWindow: class {}}));', }; try { for (const [variant, mutation] of Object.entries(cases)) { const directory = join(work, variant); mkdirSync(directory); writeFileSync(join(directory, 'bundle.js'), prefix + mutation + '\nload(1);\n})();\n'); const run = spawnSync(process.execPath, [ 'scripts/rea.mjs', 'analyze-javascript-application', directory, '--format', 'json', '--full-output', ], { encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024, env: {...process.env, HOME: home, XDG_CONFIG_HOME: config, XDG_CACHE_HOME: cache, npm_config_cache: cache}, }); if (run.status !== 0) throw Error(JSON.stringify({variant, status: run.status, error: run.error?.message, stderr: run.stderr})); const envelope = JSON.parse(run.stdout); if (envelope.ok !== true) throw Error(JSON.stringify(envelope)); const value = envelope.data.normalized_result; const nodes = value.graph.nodes.filter(node => node.kind === 'browser-window'); console.log(JSON.stringify({variant, exit: run.status, browser_windows: value.summary.browser_windows, window_nodes: nodes.length})); } } finally { rmSync(work, {recursive: true, force: true}); } JS ``` ## Expected behavior The alias replacement should produce zero Electron BrowserWindow observations through the replaced getter, matching the direct and reflective replacement controls. The intact getter and unrelated alias-property write should each retain one. The genuine literal `require('electron')` remains a source observation in every fixture; invalidating the getter identity must preserve unrelated artifact and source facts. The [Electron identity rules](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/docs/javascript-artifact-reconstruction.md#L357-L402) require proven lexical aliases, leave visibly overwritten namespace members unresolved, and check direct assignment targets through aliases while preserving unrelated member writes. The [canonical getter proof](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptBundledRequireOrigin.ts#L147-L194) requires the canonical installation and rejects visible replacements. The [existing getter tests](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/electronBundledRequireIdentity.test.ts#L25-L83) cover intact getters, several direct-root replacement forms, and an unrelated-key control. Here `copy` and `load` refer to the same function object, so the literal local `copy.n` assignment replaces `load.n` before use without requiring arbitrary runtime reachability analysis. ## Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: A successful application analysis adds a false exact Electron BrowserWindow inventory entry and graph observation for a local replacement class. The demonstrated failure is bounded to a recognized bootstrap whose canonical getter is replaced through a loader alias; shipped-application prevalence and downstream analyst decisions were not measured. - Workaround status: unverified - Workaround basis: No caller option that preserves the original artifact and returns a corrected inventory was verified. Rewriting the alias assignment into a direct assignment changes the analyzed input and serves only as a control; manual source inspection does not correct the returned inventory. ## REA version Reproduced from source baseline [62bf09809ef7bb224028916424f8f07652a20f4f](https://github.com/morluto/rea/tree/62bf09809ef7bb224028916424f8f07652a20f4f) and independently replayed after rebuilding upstream main at [d9aa06d93d95aa237927b83a433b495133302ef9](https://github.com/morluto/rea/tree/d9aa06d93d95aa237927b83a433b495133302ef9). A source-only freshness recheck of true upstream main [e2db768012079b82e6f1ff7191d475494b06d2a2](https://github.com/morluto/rea/tree/e2db768012079b82e6f1ff7191d475494b06d2a2) found no changes in the JavaScript analysis, query, public-adapter or cited contract paths compared with the replayed revision. That newer revision was inspected, not executed. The source manifest reports 6.5.0; these are repository builds, not an installed-release reproduction. Published-package applicability is NOT_ENOUGH_INFO because no installed release run or complete release-applicability proof was performed. ## Runtime and platform Repository-pinned Node v24.18.0 and npm 11.16.0 on macOS arm64, with locked dependencies retained. The current-main build completed all three preparation tasks successfully. No browser, external service, or native analysis engine is needed. ## Target and artifact context Local directory fixtures containing the complete reduced Webpack-style bootstrap above. The replacement getter returns a harmless local class. REA does not execute the fixtures. A separate JavaScript oracle executes only these generated sources with a counting Electron stub, not a real Electron engine. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Both source revisions produced: ```jsonl {"variant":"intact","exit":0,"browser_windows":1,"window_nodes":1} {"variant":"direct","exit":0,"browser_windows":0,"window_nodes":0} {"variant":"alias","exit":0,"browser_windows":1,"window_nodes":1} {"variant":"unrelated","exit":0,"browser_windows":1,"window_nodes":1} {"variant":"reflective","exit":0,"browser_windows":0,"window_nodes":0} ``` The current-main alias graph was separately read back and retains the exact, observed Electron boundary evidence described above. The independent harmless runtime oracle completed all five fixture variants without an exception. It constructed the Electron stub once for intact and unrelated, and zero times for direct, alias and reflective replacements. The successful alias program therefore constructs a local class while REA still inventories an Electron BrowserWindow. This runtime oracle does not establish real Electron engine coverage. Separate baseline verification passed all 41 existing bundled-require identity tests. Two temporary regression assertions requiring zero windows for alias replacements failed with actual length one, while their controls passed. Those temporary probes were removed; no source fix or passing restoration run is claimed. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ## Typed outcome or recovery expected Run all five unchanged fixtures through the public workflow. The alias fixture must return zero Electron BrowserWindows through its replaced getter, matching direct and reflective. Preserve one for intact and unrelated, successful application analysis, and unrelated source observations. Check both graph observations and summary counts so stale exact attribution cannot survive behind a corrected count. The implementation and regression-test design remain open. ## Analysis or suspected cause Source-derived explanation: the getter proof's write-origin callback accepts only identifiers whose own binding IDs belong to the loader binding set. The [lexical binding resolver](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptSemanticState.ts#L181-L198) resolves `copy` to its distinct binding without following its initializer. The [shared write collector](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptElectronMemberWrites.ts#L150-L169) delegates member receiver identity to that callback. The alias write is therefore absent from the getter's write inventory, leaving the old canonical proof intact. The public analysis succeeds through normal input, artifact, graph and Evidence validation; this is a retained observation in a completed result. ## Additional context Closed [#3315](https://github.com/morluto/rea/issues/3315) was fixed by merged [#3860](https://github.com/morluto/rea/pull/3860). Its original direct-root `Reflect.set` failure is restored at the tested revisions, as the reflective control above confirms. Its executed restoration matrix and added regression cases do not include this stable loader-alias receiver. This is a residual alias-specific case, not a claim that the historical fix is absent. Open [#3883](https://github.com/morluto/rea/issues/3883) replaces a computed module-table slot and uses no default getter. This fixture leaves the table unchanged and replaces only the getter through `copy.n`, so fixing that slot proof alone cannot restore this case. No MCP, installed-package, other-platform, real-Electron, full-suite or post-fix restoration run was performed. Shipped-application prevalence and the last known good revision are NOT_ENOUGH_INFO because no corpus frequency or regression boundary was measured. A source rewrite into a direct assignment is only a discriminating control; no workaround preserving the original artifact and correcting its inventory was verified.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Canonical getter proof now walks read-position-selected initializers/reference initializers to determine whether alias roots may own the loader, rather than checking alias binding ID alone. Tests include stable alias assignment and Reflect.set through alias before entry dispatch and require zero browser windows; intact/unrelated control coverage remains. No CLI/Node oracle execution performed here.

Implementation: javascriptBundledRequireOrigin.ts, electronBundledRequireIdentity.test.ts.

The relevant regression checks in electronBundledRequireIdentity.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380766425 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Canonical getter proof now walks read-position-selected initializers/reference initializers to determine whether alias roots may own the loader, rather than checking alias binding ID alone. Tests include stable alias assignment and Reflect.set through alias before entry dispatch and require zero browser windows; intact/unrelated control coverage remains. No CLI/Node oracle execution performed here. Implementation: [javascriptBundledRequireOrigin.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptBundledRequireOrigin.ts#L158), [electronBundledRequireIdentity.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/electronBundledRequireIdentity.test.ts#L79). The relevant regression checks in [electronBundledRequireIdentity.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/electronBundledRequireIdentity.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1660
No description provided.