[GH-ISSUE #1576] [Bug] Reused module IDs across Webpack runtimes cross-wire application graph relationships #530

Closed
opened 2026-10-15 18:25:55 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @dakjdakd on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1576

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
Interface: CLI
Earliest failure stage: Native analysis or reconstruction
REA version: 6.3.0, affected revision 0e19db9fce74af2081144102f5787cf90a450244
Runtime and platform: Node.js 24.18.0 / npm 11.16.0, Windows x64
Target and artifact context: A local directory containing one inert JavaScript asset with two bundler runtimes

Problem

REA recovers separate Webpack module nodes when a JavaScript asset contains two runtimes, but it can resolve their relationships through a shared, file-wide module-ID namespace. If both runtimes use IDs 1 and 2, the second registration replaces the first registration's lookup entries. The resulting application graph connects the first runtime's entrypoint and behavior to the second runtime's modules.

This is an attribution error rather than a failure to recover the factories. Both sets of module nodes are present, the source ranges still point to the correct calls, and the affected relationships are reported as resolved. Graph coverage can remain complete, so the output does not give a reader a reason to distrust those specific connections.

The corresponding fix is ready for review in https://github.com/morluto/rea/pull/1577.

Reproduction

Reproduced on REA 6.3.0, main revision 0e19db9fce74af2081144102f5787cf90a450244, using Windows and the repository-pinned Node.js 24.18.0 / npm 11.16.0 toolchain. The input is inert JavaScript; REA does not need to execute either runtime to reproduce the problem.

Create a directory containing only this bundle.js:

globalThis.webpackChunkAppA.push([
  ["a"],
  {
    1: (module, exports, req) => {
      req(2);
      fetch("/a");
    },
    2: (module) => {
      module.exports = "A";
    }
  },
  (req) => req(1)
]);

globalThis.webpackChunkAppB.push([
  ["b"],
  {
    1: (module, exports, req) => {
      req(2);
      fetch("/b");
    },
    2: (module) => {
      module.exports = "B";
    }
  },
  (req) => req(1)
]);

From a source checkout with dependencies installed and the runtime built:

npm run build:cached
node scripts/rea.mjs analyze-javascript-application /absolute/path/to/fixture --json > evidence.json

Inspect normalized_result.graph. Resolve each relationship's source_node_id and target_node_id against graph.nodes, then compare the nodes' identity.namespace and identity.key. In particular, inspect edges with properties.kind === "bundler-entry-module", the calls edge whose endpoint observation has properties.value === "/a", and the imports edges for require with specifier "2".

Observed and expected output

Relationship Observed on the affected implementation Expected
AppA entry load Target namespace webpackChunkAppB:module, key 1 Target namespace webpackChunkAppA:module, key 1
Call to /a Source is AppB module 1 Source is AppA module 1
AppA's req(2) AppB module 1 imports AppB module 2 AppA module 1 imports AppA module 2
AppB relationships AppB module ownership AppB module ownership

The AppA entry edge still carries properties.runtime: "webpackChunkAppA" and resolution_status: "resolved", even though the target node belongs to AppB. Its own metadata therefore exposes the inconsistency. The /a call similarly retains the location of the AppA source expression while selecting an AppB owner.

Removing AppB makes AppA's relationships correct. Giving AppB disjoint IDs, such as 3 and 4, also makes the basic entry, require and endpoint relationships correct. Reversing the two colliding registrations changes which runtime wins. These controls isolate registration-order-dependent identity loss from a source-position or general parser failure.

The same ownership information is used for other findings. A fuller regression fixture reproduces the issue across storage observations, Electron BrowserWindow ownership and factory-level asynchronous chunk references. Mixed Webpack/Rspack registrations also need to remain separated when their IDs overlap.

Source-level cause

The earliest loss occurs during factory recovery. JavaScriptModuleRange retains offsets, module key and require alias, but no registration runtime. recoverBundlerModule in javascriptStaticAnalysisBundler.ts creates that range, and finalizeLocatedFindings projects only module_key into the finding.

Graph construction has enough information to create distinct module identities using ${registration.runtime}:module. Its lookup ownership is narrower: moduleLookupKey identifies a module using only the asset path and module key. JavaScriptArtifactGraphBundlers.ts uses that lookup when registering modules and resolving entrypoints; JavaScriptArtifactGraphFindings.ts uses it for finding owners and require targets. Electron graph consumers also select modules through the same incomplete identity.

Consequently, changing just the entry lookup would leave source findings ambiguous. The runtime needs to survive the producing representation and every ownership consumer.

Required identity behavior

Module IDs are local to their runtime. Within one asset, the lookup identity should therefore be (asset path, runtime, module ID). Multiple chunks of the same runtime should continue sharing that runtime's module namespace. Ordinary, unbundled source modules should retain their existing identities.

These components are source-derived strings, so tuple encoding must also preserve their boundaries. For example, runtime webpackChunkA with module key x\0y must differ from runtime webpackChunkA\0x with module key y. A NUL-delimited join of three fields collapses those identities. The existing compositeKey utility can encode module and chunk lookup tuples without that ambiguity.

Impact and proposed correction

Users following entrypoints, dependencies or endpoint ownership can be sent to another application's factory despite apparently successful analysis. That undermines the core reconstruction result, particularly when investigating combined assets containing independently named runtimes. This report establishes the behavior on bounded fixtures; it does not estimate how often particular production bundles contain this layout.

The proposed fix retains runtime in recovered factory ranges, carries it into bundled JavaScript and Electron findings, and scopes all module and chunk resolution accordingly. Findings without recovered runtime ownership should not select a module from an unrelated runtime by ID alone. Regression coverage should include reversed registration order, single-runtime and disjoint-ID controls, mixed bundlers, same-runtime cross-chunk imports, and source strings containing tuple delimiters.

This is one static ownership problem. It does not require changing runtime execution, adding a bundler dependency, or expanding cross-file bootstrap reconstruction.

Originally created by @dakjdakd on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1576 ## 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 **Interface:** CLI **Earliest failure stage:** Native analysis or reconstruction **REA version:** 6.3.0, affected revision `0e19db9fce74af2081144102f5787cf90a450244` **Runtime and platform:** Node.js 24.18.0 / npm 11.16.0, Windows x64 **Target and artifact context:** A local directory containing one inert JavaScript asset with two bundler runtimes ## Problem REA recovers separate Webpack module nodes when a JavaScript asset contains two runtimes, but it can resolve their relationships through a shared, file-wide module-ID namespace. If both runtimes use IDs `1` and `2`, the second registration replaces the first registration's lookup entries. The resulting application graph connects the first runtime's entrypoint and behavior to the second runtime's modules. This is an attribution error rather than a failure to recover the factories. Both sets of module nodes are present, the source ranges still point to the correct calls, and the affected relationships are reported as resolved. Graph coverage can remain `complete`, so the output does not give a reader a reason to distrust those specific connections. The corresponding fix is ready for review in https://github.com/morluto/rea/pull/1577. ## Reproduction Reproduced on REA 6.3.0, main revision `0e19db9fce74af2081144102f5787cf90a450244`, using Windows and the repository-pinned Node.js 24.18.0 / npm 11.16.0 toolchain. The input is inert JavaScript; REA does not need to execute either runtime to reproduce the problem. Create a directory containing only this `bundle.js`: ```js globalThis.webpackChunkAppA.push([ ["a"], { 1: (module, exports, req) => { req(2); fetch("/a"); }, 2: (module) => { module.exports = "A"; } }, (req) => req(1) ]); globalThis.webpackChunkAppB.push([ ["b"], { 1: (module, exports, req) => { req(2); fetch("/b"); }, 2: (module) => { module.exports = "B"; } }, (req) => req(1) ]); ``` From a source checkout with dependencies installed and the runtime built: ```sh npm run build:cached node scripts/rea.mjs analyze-javascript-application /absolute/path/to/fixture --json > evidence.json ``` Inspect `normalized_result.graph`. Resolve each relationship's `source_node_id` and `target_node_id` against `graph.nodes`, then compare the nodes' `identity.namespace` and `identity.key`. In particular, inspect edges with `properties.kind === "bundler-entry-module"`, the `calls` edge whose endpoint observation has `properties.value === "/a"`, and the `imports` edges for `require` with specifier `"2"`. ## Observed and expected output | Relationship | Observed on the affected implementation | Expected | | --- | --- | --- | | AppA entry load | Target namespace `webpackChunkAppB:module`, key `1` | Target namespace `webpackChunkAppA:module`, key `1` | | Call to `/a` | Source is AppB module `1` | Source is AppA module `1` | | AppA's `req(2)` | AppB module `1` imports AppB module `2` | AppA module `1` imports AppA module `2` | | AppB relationships | AppB module ownership | AppB module ownership | The AppA entry edge still carries `properties.runtime: "webpackChunkAppA"` and `resolution_status: "resolved"`, even though the target node belongs to AppB. Its own metadata therefore exposes the inconsistency. The `/a` call similarly retains the location of the AppA source expression while selecting an AppB owner. Removing AppB makes AppA's relationships correct. Giving AppB disjoint IDs, such as `3` and `4`, also makes the basic entry, require and endpoint relationships correct. Reversing the two colliding registrations changes which runtime wins. These controls isolate registration-order-dependent identity loss from a source-position or general parser failure. The same ownership information is used for other findings. A fuller regression fixture reproduces the issue across storage observations, Electron `BrowserWindow` ownership and factory-level asynchronous chunk references. Mixed Webpack/Rspack registrations also need to remain separated when their IDs overlap. ## Source-level cause The earliest loss occurs during factory recovery. [`JavaScriptModuleRange`](https://github.com/morluto/rea/blob/0e19db9fce74af2081144102f5787cf90a450244/src/domain/javascript/javascriptStaticAnalysisState.ts) retains offsets, module key and require alias, but no registration runtime. `recoverBundlerModule` in `javascriptStaticAnalysisBundler.ts` creates that range, and [`finalizeLocatedFindings`](https://github.com/morluto/rea/blob/0e19db9fce74af2081144102f5787cf90a450244/src/domain/javascript/javascriptStaticAnalysisFindings.ts) projects only `module_key` into the finding. Graph construction has enough information to create distinct module identities using `${registration.runtime}:module`. Its lookup ownership is narrower: [`moduleLookupKey`](https://github.com/morluto/rea/blob/0e19db9fce74af2081144102f5787cf90a450244/src/application/javascript/JavaScriptArtifactGraphContext.ts) identifies a module using only the asset path and module key. `JavaScriptArtifactGraphBundlers.ts` uses that lookup when registering modules and resolving entrypoints; `JavaScriptArtifactGraphFindings.ts` uses it for finding owners and require targets. Electron graph consumers also select modules through the same incomplete identity. Consequently, changing just the entry lookup would leave source findings ambiguous. The runtime needs to survive the producing representation and every ownership consumer. ## Required identity behavior Module IDs are local to their runtime. Within one asset, the lookup identity should therefore be `(asset path, runtime, module ID)`. Multiple chunks of the same runtime should continue sharing that runtime's module namespace. Ordinary, unbundled source modules should retain their existing identities. These components are source-derived strings, so tuple encoding must also preserve their boundaries. For example, runtime `webpackChunkA` with module key `x\0y` must differ from runtime `webpackChunkA\0x` with module key `y`. A NUL-delimited join of three fields collapses those identities. The existing `compositeKey` utility can encode module and chunk lookup tuples without that ambiguity. ## Impact and proposed correction Users following entrypoints, dependencies or endpoint ownership can be sent to another application's factory despite apparently successful analysis. That undermines the core reconstruction result, particularly when investigating combined assets containing independently named runtimes. This report establishes the behavior on bounded fixtures; it does not estimate how often particular production bundles contain this layout. The proposed fix retains runtime in recovered factory ranges, carries it into bundled JavaScript and Electron findings, and scopes all module and chunk resolution accordingly. Findings without recovered runtime ownership should not select a module from an unrelated runtime by ID alone. Regression coverage should include reversed registration order, single-runtime and disjoint-ID controls, mixed bundlers, same-runtime cross-chunk imports, and source strings containing tuple delimiters. This is one static ownership problem. It does not require changing runtime execution, adding a bundler dependency, or expanding cross-file bootstrap reconstruction.
bsakaguchi 2026-10-15 18:25:55 +00:00
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#530
No description provided.