[GH-ISSUE #3315] [Bug] Replaced or deleted Webpack export getters still produce Electron API facts #1187

Closed
opened 2026-10-15 18:31:50 +00:00 by bsakaguchi · 1 comment
Owner

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

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 continues to attribute a BrowserWindow call to Electron after the canonical Webpack default-export getter is replaced with a local implementation through Reflect.set. Deleting the getter leaves the same attribution. An equivalent direct assignment correctly suppresses the Electron fact. All four analysis commands succeed, and the reflective and deleted variants each contribute one BrowserWindow fact, matching the intact getter. Only the direct-assignment variant contributes zero.

Reproduction

  1. Create a clean source checkout, select the reproduced baseline, install its locked dependencies, and build the runtime:
git clone https://github.com/morluto/rea.git rea-getter-repro
cd rea-getter-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. From the repository root, create all four fixtures, analyze them, and print their BrowserWindow counts with the following commands. The files are analyzed as inert JavaScript; no Electron process or external engine is required.
workdir="$(mktemp -d)"
mkdir -p "$workdir/home"
node --input-type=module - "$workdir" <<'JS'
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
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 = {
  reflective: "Reflect.set(load, 'n', mod => () => ({ BrowserWindow: class {} }));",
  deleted: 'delete load.n;',
  direct: 'load.n = mod => () => ({ BrowserWindow: class {} });',
  intact: ''
};
for (const [name, write] of Object.entries(cases)) {
  const directory = join(process.argv[2], name);
  mkdirSync(directory);
  writeFileSync(join(directory, 'bundle.js'), prefix + write + '\nload(1);\n})();\n');
}
JS
for variant in reflective deleted direct intact; do
  HOME="$workdir/home" node scripts/rea.mjs analyze-javascript-application "$workdir/$variant" --json > "$workdir/$variant.json"
done
node --input-type=module - "$workdir" <<'JS'
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
for (const variant of ['reflective', 'deleted', 'direct', 'intact']) {
  const result = JSON.parse(readFileSync(join(process.argv[2], `${variant}.json`), 'utf8')).normalized_result;
  console.log(JSON.stringify({ variant, browser_windows: result.summary.browser_windows }));
}
JS
  1. Observe the four output rows in Relevant output or evidence. The reflective and deleted variants retain the Electron attribution despite their visible writes.

Expected behavior

The reflective and deleted variants must not contribute a BrowserWindow fact through the former getter identity. The reflective variant returns a locally defined class, and the deleted variant no longer has a callable getter. Their Electron attribution should remain unresolved, as it does for the direct assignment. The intact canonical getter should continue to produce one BrowserWindow fact.

docs/javascript-artifact-reconstruction.md:355 requires reassignments and visibly overwritten namespace members to remain unresolved, and docs/javascript-artifact-reconstruction.md:377 includes delete targets in identity checks. src/domain/javascript/electronBundledRequireIdentity.test.ts:25 pins Electron identity through the canonical Webpack getter and rejects replaced getters in the following cases.

Impact and scope

Impact level: P2, supported. A supported visible write causes static analysis to attribute a local class or unavailable call to Electron, corrupting the reported application boundary inventory. The failure is bounded to applications that alter the recovered Webpack getter through the missed write forms.

Workaround status: unverified. Direct assignment was a successful discriminating control, but rewriting the analyzed application would change its source Evidence. No workaround that preserves the supplied artifact and produces the correct REA inventory was verified.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0, macOS 27.0.1, prebuilt source-checkout runtime. Architecture was not recorded in the reproduction evidence.

Target and artifact context

Local inert JavaScript directory fixtures containing a Webpack-style module table, an Electron external factory, and a canonical default-export getter. The smaller fixture removes the cache and IPC call and preserves the BrowserWindow failure. A larger fixture also reproduced the same stale attribution for an IPC handle call, while direct assignment removed both Electron observations. No Electron process or external engine is required.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed CLI reproduction succeeded for all four variants and printed:

{"variant":"reflective","browser_windows":1}
{"variant":"deleted","browser_windows":1}
{"variant":"direct","browser_windows":0}
{"variant":"intact","browser_windows":1}

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

Typed outcome or recovery expected

Run all four variants unchanged. The reflective, deleted, and direct variants must produce zero BrowserWindow facts through this getter, while the intact variant must retain one. Preserve the artifact inventory and unrelated source observations, and retain explicit uncertainty for the unsupported getter identity rather than replacing the whole application result with an error.

Analysis or suspected cause

Source-derived reasoning: src/domain/javascript/javascriptBundledRequireOrigin.ts:139 owns the canonical getter proof. Its write inventory at src/domain/javascript/javascriptBundledRequireOrigin.ts:149 admits assignments, and its reflective-write scan at src/domain/javascript/javascriptBundledRequireOrigin.ts:165 checks selected Object methods. These checks leave the observed Reflect.set and delete writes outside the rejection path. The executed CLI output establishes the stale attribution; this source inspection explains a suspected cause.

Additional context

  • Related #2932, closed as completed, concerns missing Electron recovery through intact Webpack registry and default-interop helpers; this report concerns false attribution after the helper is visibly replaced or deleted.
  • Related #1842, closed as completed, concerns helper-name-based attribution through unrelated conversion bodies; this report concerns invalidation of an initially canonical Webpack getter.
  • Related #1909, closed as completed, concerns nested invocation forms hiding writes to Rolldown intrinsic dependencies; these fixtures directly replace or delete the Webpack loader's getter member.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3315 ### 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` continues to attribute a BrowserWindow call to Electron after the canonical Webpack default-export getter is replaced with a local implementation through `Reflect.set`. Deleting the getter leaves the same attribution. An equivalent direct assignment correctly suppresses the Electron fact. All four analysis commands succeed, and the reflective and deleted variants each contribute one BrowserWindow fact, matching the intact getter. Only the direct-assignment variant contributes zero. ### Reproduction 1. Create a clean source checkout, select the reproduced baseline, install its locked dependencies, and build the runtime: ```sh git clone https://github.com/morluto/rea.git rea-getter-repro cd rea-getter-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. From the repository root, create all four fixtures, analyze them, and print their BrowserWindow counts with the following commands. The files are analyzed as inert JavaScript; no Electron process or external engine is required. ```sh workdir="$(mktemp -d)" mkdir -p "$workdir/home" node --input-type=module - "$workdir" <<'JS' import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; 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 = { reflective: "Reflect.set(load, 'n', mod => () => ({ BrowserWindow: class {} }));", deleted: 'delete load.n;', direct: 'load.n = mod => () => ({ BrowserWindow: class {} });', intact: '' }; for (const [name, write] of Object.entries(cases)) { const directory = join(process.argv[2], name); mkdirSync(directory); writeFileSync(join(directory, 'bundle.js'), prefix + write + '\nload(1);\n})();\n'); } JS for variant in reflective deleted direct intact; do HOME="$workdir/home" node scripts/rea.mjs analyze-javascript-application "$workdir/$variant" --json > "$workdir/$variant.json" done node --input-type=module - "$workdir" <<'JS' import { readFileSync } from 'node:fs'; import { join } from 'node:path'; for (const variant of ['reflective', 'deleted', 'direct', 'intact']) { const result = JSON.parse(readFileSync(join(process.argv[2], `${variant}.json`), 'utf8')).normalized_result; console.log(JSON.stringify({ variant, browser_windows: result.summary.browser_windows })); } JS ``` 3. Observe the four output rows in Relevant output or evidence. The reflective and deleted variants retain the Electron attribution despite their visible writes. ### Expected behavior The `reflective` and `deleted` variants must not contribute a BrowserWindow fact through the former getter identity. The reflective variant returns a locally defined class, and the deleted variant no longer has a callable getter. Their Electron attribution should remain unresolved, as it does for the direct assignment. The intact canonical getter should continue to produce one BrowserWindow fact. `docs/javascript-artifact-reconstruction.md:355` requires reassignments and visibly overwritten namespace members to remain unresolved, and `docs/javascript-artifact-reconstruction.md:377` includes delete targets in identity checks. `src/domain/javascript/electronBundledRequireIdentity.test.ts:25` pins Electron identity through the canonical Webpack getter and rejects replaced getters in the following cases. ### Impact and scope Impact level: P2, supported. A supported visible write causes static analysis to attribute a local class or unavailable call to Electron, corrupting the reported application boundary inventory. The failure is bounded to applications that alter the recovered Webpack getter through the missed write forms. Workaround status: unverified. Direct assignment was a successful discriminating control, but rewriting the analyzed application would change its source Evidence. No workaround that preserves the supplied artifact and produces the correct REA inventory was verified. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0, macOS 27.0.1, prebuilt source-checkout runtime. Architecture was not recorded in the reproduction evidence. ### Target and artifact context Local inert JavaScript directory fixtures containing a Webpack-style module table, an Electron external factory, and a canonical default-export getter. The smaller fixture removes the cache and IPC call and preserves the BrowserWindow failure. A larger fixture also reproduced the same stale attribution for an IPC `handle` call, while direct assignment removed both Electron observations. No Electron process or external engine is required. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The executed CLI reproduction succeeded for all four variants and printed: ```text {"variant":"reflective","browser_windows":1} {"variant":"deleted","browser_windows":1} {"variant":"direct","browser_windows":0} {"variant":"intact","browser_windows":1} ``` Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. ### Typed outcome or recovery expected Run all four variants unchanged. The reflective, deleted, and direct variants must produce zero BrowserWindow facts through this getter, while the intact variant must retain one. Preserve the artifact inventory and unrelated source observations, and retain explicit uncertainty for the unsupported getter identity rather than replacing the whole application result with an error. ### Analysis or suspected cause Source-derived reasoning: `src/domain/javascript/javascriptBundledRequireOrigin.ts:139` owns the canonical getter proof. Its write inventory at `src/domain/javascript/javascriptBundledRequireOrigin.ts:149` admits assignments, and its reflective-write scan at `src/domain/javascript/javascriptBundledRequireOrigin.ts:165` checks selected Object methods. These checks leave the observed `Reflect.set` and delete writes outside the rejection path. The executed CLI output establishes the stale attribution; this source inspection explains a suspected cause. ### Additional context - Related [#2932](https://github.com/morluto/rea/issues/2932), closed as completed, concerns missing Electron recovery through intact Webpack registry and default-interop helpers; this report concerns false attribution after the helper is visibly replaced or deleted. - Related [#1842](https://github.com/morluto/rea/issues/1842), closed as completed, concerns helper-name-based attribution through unrelated conversion bodies; this report concerns invalidation of an initially canonical Webpack getter. - Related [#1909](https://github.com/morluto/rea/issues/1909), closed as completed, concerns nested invocation forms hiding writes to Rolldown intrinsic dependencies; these fixtures directly replace or delete the Webpack loader's getter member.
Author
Owner

@CassieFox23 commented on GitHub (Oct 15, 2026):

TL;DR: Confirmed on Linux. Main still has this bug. A getter swapped by Reflect.set or deleted still counts as Electron.

Results

I ran your recipe on main 7d1e4d0e2 (#3778), package version 6.5.0. It is 164 commits after your base 8dde6370. The file src/domain/javascript/javascriptBundledRequireOrigin.ts has no commit since your base. Its scan still checks only member assignments and four Object methods. It has no check for Reflect.set or delete.

I ran all four variants on Node 24.18.0 and on Node 26.11.1. Every analysis command exited 0.

Variant browser_windows Same as your report
reflective 1 yes
deleted 1 yes
direct 0 yes
intact 1 yes

Both Node versions gave the same four rows. The reflective and deleted rows should read 0, like the direct row.

I found no open pull request that mentions this issue.

Not run

  • Your base 8dde6370 (main already reproduces it).
  • Your larger fixture with the IPC handle call. The issue gives no recipe for it.
  • macOS and Windows.

Thanks, coygeek, for the clear report and the control.

Commands
npm ci
npm run build:cached
# per Node version, fresh HOME each time
mkdir -p /tmp/i3315/work-n24/home
node gen.mjs /tmp/i3315/work-n24   # your first heredoc, verbatim
for v in reflective deleted direct intact; do
  env -C /tmp/i3315/main HOME=/tmp/i3315/work-n24/home \
    node scripts/rea.mjs analyze-javascript-application \
    /tmp/i3315/work-n24/$v --json > /tmp/i3315/work-n24/$v.json
done
node count.mjs /tmp/i3315/work-n24   # your second heredoc, verbatim

I ran each build and run under nice and a 6 GB memory limit. The box was shared with a benchmark, so timings are not reported.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0 (build and runs), 26.11.1 (runs); npm 11.16.0
  • Main / your base: 7d1e4d0e2 / 8dde6370

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6182467148 --> @CassieFox23 commented on GitHub (Oct 15, 2026): **TL;DR:** Confirmed on Linux. Main still has this bug. A getter swapped by `Reflect.set` or deleted still counts as Electron. ### Results I ran your recipe on main 7d1e4d0e2 (#3778), package version 6.5.0. It is 164 commits after your base 8dde6370. The file `src/domain/javascript/javascriptBundledRequireOrigin.ts` has no commit since your base. Its scan still checks only member assignments and four `Object` methods. It has no check for `Reflect.set` or `delete`. I ran all four variants on Node 24.18.0 and on Node 26.11.1. Every analysis command exited 0. | Variant | `browser_windows` | Same as your report | |---|---|---| | reflective | 1 | yes | | deleted | 1 | yes | | direct | 0 | yes | | intact | 1 | yes | Both Node versions gave the same four rows. The reflective and deleted rows should read 0, like the direct row. I found no open pull request that mentions this issue. ### Not run - Your base 8dde6370 (main already reproduces it). - Your larger fixture with the IPC `handle` call. The issue gives no recipe for it. - macOS and Windows. Thanks, coygeek, for the clear report and the control. <details> <summary>Commands</summary> ```sh npm ci npm run build:cached # per Node version, fresh HOME each time mkdir -p /tmp/i3315/work-n24/home node gen.mjs /tmp/i3315/work-n24 # your first heredoc, verbatim for v in reflective deleted direct intact; do env -C /tmp/i3315/main HOME=/tmp/i3315/work-n24/home \ node scripts/rea.mjs analyze-javascript-application \ /tmp/i3315/work-n24/$v --json > /tmp/i3315/work-n24/$v.json done node count.mjs /tmp/i3315/work-n24 # your second heredoc, verbatim ``` I ran each build and run under nice and a 6 GB memory limit. The box was shared with a benchmark, so timings are not reported. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 (build and runs), 26.11.1 (runs); npm 11.16.0 - Main / your base: 7d1e4d0e2 / 8dde6370 </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
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#1187
No description provided.