[GH-ISSUE #8943] [Bug] analyze-javascript-application resolves a replaced loader after a quoted unscopables prototype setter #7795

Open
opened 2026-11-05 01:35:36 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Nov 5, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8943

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 a resolved CommonJS dependency after a with assignment has replaced the loader. The trigger is a quoted, non-computed "__proto__" setter in the with object's Symbol.unscopables literal. Its prototype supplies require: true, so the assignment reaches the outer loader. Separately executing the owned fixture in Node prints LOCAL; REA still emits a semantic require import to dep.cjs, with resolution_status: "resolved" and local edge coverage complete.

The equivalent unquoted prototype setter and an explicit own require: true entry correctly leave that later dependency unresolved. Controls where the assignment stays on the with object retain the real dependency. Both the provided source build and a separately installed latest published package reproduce the failure. REA analyzes these fixtures statically; the separate Node runs supply the runtime comparison.

Reproduction

Use Node 24.22.1 and a built checkout at c3647ade1e557c4179a8b825a37e0c2193cd2526. Fresh-checkout preparation is npm ci followed by npm run build:cached; verification used an already prepared build and did not rerun those preparation commands. From the checkout root, run this reproduction. It creates only temporary fixtures, isolates the consumer home and environment, calls the real CLI, and removes its temporary fixtures afterward:

python3 - <<'PY'
import json
import os
from pathlib import Path
import subprocess
import tempfile

runner = Path("scripts/rea.mjs").resolve()
variants = {
    "quoted": '{"__proto__": {require: true}}',
    "unquoted": '{__proto__: {require: true}}',
    "own-true": '{require: true}',
    "own-false": '{require: false}',
    "computed": '{["__proto__"]: {require: true}}',
}
with tempfile.TemporaryDirectory(prefix="rea-unscopables-") as work:
    root = Path(work)
    home = root / "home"
    home.mkdir()
    env = {"PATH": os.environ["PATH"], "HOME": str(home), "TMPDIR": str(root)}
    for name, unscopables in variants.items():
        app = root / name
        app.mkdir()
        (app / "package.json").write_text(json.dumps({
            "name": "fixture", "version": "1.0.0",
            "main": "index.cjs", "type": "commonjs",
        }))
        (app / "dep.cjs").write_text('module.exports = "OUTER";\n')
        (app / "index.cjs").write_text(
            'with ({require: null, [Symbol.unscopables]: ' + unscopables + '}) {\n'
            '  require = () => "LOCAL";\n'
            '}\nconsole.log(require("./dep.cjs"));\n'
        )
        runtime = subprocess.run(
            ["node", str(app / "index.cjs")], env=env,
            capture_output=True, text=True, timeout=30, check=True,
        )
        analysis = subprocess.run(
            ["node", str(runner), "analyze-javascript-application", str(app),
             "--format", "json", "--full-output"], env=env,
            capture_output=True, text=True, timeout=60, check=True,
        )
        result = json.loads(analysis.stdout)
        assert result["ok"] is True
        edges = result["data"]["normalized_result"]["graph"]["edges"]
        requires = [
            {"specifier": e["properties"]["specifier"],
             "resolved_path": e["properties"]["resolved_path"],
             "resolution_status": e["properties"]["resolution_status"],
             "coverage": e["evidence"]["coverage"]["status"]}
            for e in edges
            if e.get("properties", {}).get("module_link_kind") == "require"
        ]
        print(name, "runtime=" + runtime.stdout.strip(),
              "require_edges=" + json.dumps(requires))
PY

The quoted case's generated index.cjs is:

with ({require: null, [Symbol.unscopables]: {"__proto__": {require: true}}}) {
  require = () => "LOCAL";
}
console.log(require("./dep.cjs"));

The companion dep.cjs exports "OUTER". The manifest selects CommonJS and index.cjs as its entry point. All runtime and analysis subprocesses exited successfully. The five-case matrix reproduced twice through the source CLI, including the exact script above, and once through the separately installed published package.

Expected behavior

A write that can reach the outer require must prevent the later call from being identified as an unchanged CommonJS loader. The quoted and unquoted non-computed prototype setters should produce equivalent conservative dependency handling. Omitting the resolved dependency or retaining the unresolved call is sufficient; exact execution modeling is unnecessary.

Preserve the positive controls: an own false unscopables entry leaves the with object's binding available, and a computed ["__proto__"] key creates an own data property rather than setting the unscopables prototype. In both controls the assignment stays on the with object, and the later real CommonJS dependency should remain resolved.

Impact and scope

P2: a bounded, consequential static-analysis error for a valid non-strict CommonJS source shape. The caller receives a false resolved dependency with high-confidence relationship evidence and complete local edge coverage. The analysis also retains a dynamic-scope unknown elsewhere; this report does not claim globally complete coverage or runtime execution by REA.

No external application incident, prevalence measurement or severe downstream harm was established. No input-preserving public workaround was verified. Replacing the quoted setter with its unquoted spelling demonstrates the working control but changes the analyzed artifact. This is an analysis correctness defect; no security boundary is crossed.

REA version

Source build: rea-agents 6.8.0 at c3647ade1e557c4179a8b825a37e0c2193cd2526. Separately installed npm rea-agents@6.8.0, reporting 6.8.0, also reproduces. npm view rea-agents version returned 6.8.0.

Fetched origin/main at 71f680d7c1aa7a45ba0325fe87b104744b0c105f retains the same affected implementation. That revision was inspected, not rebuilt or executed.

Runtime and platform

Node.js v24.22.1; Darwin arm64. No Hopper, Ghidra or other native provider was needed. Other platforms and MCP were not exercised.

Target and artifact context

Owned synthetic directory applications, each containing a manifest, a non-strict CommonJS index.cjs, and dep.cjs. REA parsed their inert UTF-8 source through the normal isolated analysis worker. Separate Node execution was limited to these owned fixtures.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The exact final reproduction printed:

quoted runtime=LOCAL require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]
unquoted runtime=LOCAL require_edges=[]
own-true runtime=LOCAL require_edges=[]
own-false runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]
computed runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]

The published package produced the same five semantic-edge outcomes. The incorrect quoted-case semantic relationship contains:

{
  "relation": "imports",
  "properties": {
    "module_link_kind": "require",
    "specifier": "./dep.cjs",
    "resolved_path": "dep.cjs",
    "resolution_status": "resolved"
  },
  "evidence": {
    "authority": "static-relationship-inference",
    "state": "inferred",
    "confidence": "high",
    "coverage": {"status": "complete", "truncated": false, "omitted_count": 0, "limits": []}
  }
}

This excerpt selects fields from the real CLI JSON; the complete edge additionally retains artifact identity, source location and static-analysis limitations. A separate static kind: "require" relationship also resolves dep.cjs in this case. Generic limitations about execution do not justify identifying a demonstrably replaced function as the CommonJS loader.

Expected source: the existing module-link controls omit uncertain or shadowed require links. The origin guard rejects prior ambient loader writes, and the assignment owner distinguishes captured writes from possible outer writes. These are source-derived contract references; the CLI and Node outputs above are direct observations.

The existing focused suite npm run test:focused -- src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts passed all 33 tests. It does not contain the quoted-prototype write fixture. No whole-suite result is claimed.

Typed outcome or recovery expected

For the unchanged quoted fixture, stop asserting the resolved semantic require relationship to dep.cjs; retaining an unresolved static call is acceptable. Keep the unquoted and own-true controls unresolved, and retain the resolved dependency in the own-false and computed-key controls. A general dynamic-scope limitation elsewhere in the result does not repair the false local loader inference.

Analysis or suspected cause

Source-derived: semanticWithPrototypeKind recognizes a non-computed prototype setter only when its key is an identifier. The quoted setter therefore reaches its ordinary return. semanticWithRequireLookup then treats the missing own unscopables.require as proof of capture, despite the custom prototype's inherited true value. The neighboring property helpers already normalize string-literal keys.

addAssignment discards captured writes. As a result, semanticRequireOrigin finds no recorded ambient replacement at the later call. The semantic projection emits a require link, and module relationship construction resolves it to the companion file.

The classifier, assignment owner, origin guards, semantic projection, worker and application workflow files inspected here are unchanged between the baseline and fetched main. No repair was implemented or tested.

Additional context

Open and closed issue/PR searches included unscopables, Symbol.unscopables, the prototype helper name, quoted prototype keys, loader replacement and with-scope binding terms. No current open exact duplicate was found.

Closed #3982 and #4072 concern earlier loader replacement handling. The issue closure records its shared-ownership fix; the PR was closed as superseded. Its explicit unscopables-entry cases do not cover a quoted custom prototype setter. Closed #2890 and #2892 address local shadowing. Closed #7418 and merged #7914 address uncertain member writes to outer objects. This fixture instead loses a scalar loader assignment because its lookup is incorrectly classified as captured.

Read-only calls inside a with body alone are not the decisive failure: both quoted and unquoted custom-prototype read-only controls remain unresolved. The reproduction above places a loader write inside with and its later call outside, exposing the concrete consequence without requiring exact resolution of the with-body read.

Originally created by @coygeek on GitHub (Nov 5, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8943 ### 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 a resolved CommonJS dependency after a `with` assignment has replaced the loader. The trigger is a quoted, non-computed `"__proto__"` setter in the with object's `Symbol.unscopables` literal. Its prototype supplies `require: true`, so the assignment reaches the outer loader. Separately executing the owned fixture in Node prints `LOCAL`; REA still emits a semantic `require` import to `dep.cjs`, with `resolution_status: "resolved"` and local edge coverage `complete`. The equivalent unquoted prototype setter and an explicit own `require: true` entry correctly leave that later dependency unresolved. Controls where the assignment stays on the with object retain the real dependency. Both the provided source build and a separately installed latest published package reproduce the failure. REA analyzes these fixtures statically; the separate Node runs supply the runtime comparison. ### Reproduction Use Node 24.22.1 and a built checkout at `c3647ade1e557c4179a8b825a37e0c2193cd2526`. Fresh-checkout preparation is `npm ci` followed by `npm run build:cached`; verification used an already prepared build and did not rerun those preparation commands. From the checkout root, run this reproduction. It creates only temporary fixtures, isolates the consumer home and environment, calls the real CLI, and removes its temporary fixtures afterward: ```sh python3 - <<'PY' import json import os from pathlib import Path import subprocess import tempfile runner = Path("scripts/rea.mjs").resolve() variants = { "quoted": '{"__proto__": {require: true}}', "unquoted": '{__proto__: {require: true}}', "own-true": '{require: true}', "own-false": '{require: false}', "computed": '{["__proto__"]: {require: true}}', } with tempfile.TemporaryDirectory(prefix="rea-unscopables-") as work: root = Path(work) home = root / "home" home.mkdir() env = {"PATH": os.environ["PATH"], "HOME": str(home), "TMPDIR": str(root)} for name, unscopables in variants.items(): app = root / name app.mkdir() (app / "package.json").write_text(json.dumps({ "name": "fixture", "version": "1.0.0", "main": "index.cjs", "type": "commonjs", })) (app / "dep.cjs").write_text('module.exports = "OUTER";\n') (app / "index.cjs").write_text( 'with ({require: null, [Symbol.unscopables]: ' + unscopables + '}) {\n' ' require = () => "LOCAL";\n' '}\nconsole.log(require("./dep.cjs"));\n' ) runtime = subprocess.run( ["node", str(app / "index.cjs")], env=env, capture_output=True, text=True, timeout=30, check=True, ) analysis = subprocess.run( ["node", str(runner), "analyze-javascript-application", str(app), "--format", "json", "--full-output"], env=env, capture_output=True, text=True, timeout=60, check=True, ) result = json.loads(analysis.stdout) assert result["ok"] is True edges = result["data"]["normalized_result"]["graph"]["edges"] requires = [ {"specifier": e["properties"]["specifier"], "resolved_path": e["properties"]["resolved_path"], "resolution_status": e["properties"]["resolution_status"], "coverage": e["evidence"]["coverage"]["status"]} for e in edges if e.get("properties", {}).get("module_link_kind") == "require" ] print(name, "runtime=" + runtime.stdout.strip(), "require_edges=" + json.dumps(requires)) PY ``` The quoted case's generated `index.cjs` is: ```js with ({require: null, [Symbol.unscopables]: {"__proto__": {require: true}}}) { require = () => "LOCAL"; } console.log(require("./dep.cjs")); ``` The companion `dep.cjs` exports `"OUTER"`. The manifest selects CommonJS and `index.cjs` as its entry point. All runtime and analysis subprocesses exited successfully. The five-case matrix reproduced twice through the source CLI, including the exact script above, and once through the separately installed published package. ### Expected behavior A write that can reach the outer `require` must prevent the later call from being identified as an unchanged CommonJS loader. The quoted and unquoted non-computed prototype setters should produce equivalent conservative dependency handling. Omitting the resolved dependency or retaining the unresolved call is sufficient; exact execution modeling is unnecessary. Preserve the positive controls: an own false unscopables entry leaves the with object's binding available, and a computed `["__proto__"]` key creates an own data property rather than setting the unscopables prototype. In both controls the assignment stays on the with object, and the later real CommonJS dependency should remain resolved. ### Impact and scope P2: a bounded, consequential static-analysis error for a valid non-strict CommonJS source shape. The caller receives a false resolved dependency with high-confidence relationship evidence and complete local edge coverage. The analysis also retains a dynamic-scope unknown elsewhere; this report does not claim globally complete coverage or runtime execution by REA. No external application incident, prevalence measurement or severe downstream harm was established. No input-preserving public workaround was verified. Replacing the quoted setter with its unquoted spelling demonstrates the working control but changes the analyzed artifact. This is an analysis correctness defect; no security boundary is crossed. ### REA version Source build: `rea-agents` 6.8.0 at `c3647ade1e557c4179a8b825a37e0c2193cd2526`. Separately installed npm `rea-agents@6.8.0`, reporting `6.8.0`, also reproduces. `npm view rea-agents version` returned `6.8.0`. Fetched `origin/main` at `71f680d7c1aa7a45ba0325fe87b104744b0c105f` retains the same affected implementation. That revision was inspected, not rebuilt or executed. ### Runtime and platform Node.js `v24.22.1`; Darwin arm64. No Hopper, Ghidra or other native provider was needed. Other platforms and MCP were not exercised. ### Target and artifact context Owned synthetic directory applications, each containing a manifest, a non-strict CommonJS `index.cjs`, and `dep.cjs`. REA parsed their inert UTF-8 source through the normal isolated analysis worker. Separate Node execution was limited to these owned fixtures. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The exact final reproduction printed: ```text quoted runtime=LOCAL require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}] unquoted runtime=LOCAL require_edges=[] own-true runtime=LOCAL require_edges=[] own-false runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}] computed runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}] ``` The published package produced the same five semantic-edge outcomes. The incorrect quoted-case semantic relationship contains: ```json { "relation": "imports", "properties": { "module_link_kind": "require", "specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved" }, "evidence": { "authority": "static-relationship-inference", "state": "inferred", "confidence": "high", "coverage": {"status": "complete", "truncated": false, "omitted_count": 0, "limits": []} } } ``` This excerpt selects fields from the real CLI JSON; the complete edge additionally retains artifact identity, source location and static-analysis limitations. A separate static `kind: "require"` relationship also resolves `dep.cjs` in this case. Generic limitations about execution do not justify identifying a demonstrably replaced function as the CommonJS loader. Expected source: [the existing module-link controls](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts#L268) omit uncertain or shadowed require links. [The origin guard](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticRequireOrigin.ts#L1557) rejects prior ambient loader writes, and [the assignment owner](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticAnalysis.ts#L2339) distinguishes captured writes from possible outer writes. These are source-derived contract references; the CLI and Node outputs above are direct observations. The existing focused suite `npm run test:focused -- src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts` passed all 33 tests. It does not contain the quoted-prototype write fixture. No whole-suite result is claimed. ### Typed outcome or recovery expected For the unchanged quoted fixture, stop asserting the resolved semantic require relationship to `dep.cjs`; retaining an unresolved static call is acceptable. Keep the unquoted and own-true controls unresolved, and retain the resolved dependency in the own-false and computed-key controls. A general dynamic-scope limitation elsewhere in the result does not repair the false local loader inference. ### Analysis or suspected cause Source-derived: [semanticWithPrototypeKind](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticState.ts#L496) recognizes a non-computed prototype setter only when its key is an identifier. The quoted setter therefore reaches its `ordinary` return. [semanticWithRequireLookup](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticState.ts#L437) then treats the missing own unscopables.require as proof of capture, despite the custom prototype's inherited true value. The neighboring property helpers already normalize string-literal keys. [addAssignment](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticAnalysis.ts#L2339) discards captured writes. As a result, [semanticRequireOrigin](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticRequireOrigin.ts#L1570) finds no recorded ambient replacement at the later call. [The semantic projection](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/domain/javascript/javascriptSemanticProjection.ts#L451) emits a require link, and [module relationship construction](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/application/javascript/JavaScriptModuleRelationships.ts#L259) resolves it to the companion file. The classifier, assignment owner, origin guards, semantic projection, worker and application workflow files inspected here are unchanged between the baseline and fetched main. No repair was implemented or tested. ### Additional context Open and closed issue/PR searches included `unscopables`, `Symbol.unscopables`, the prototype helper name, quoted prototype keys, loader replacement and with-scope binding terms. No current open exact duplicate was found. Closed [#3982](https://github.com/morluto/rea/issues/3982) and [#4072](https://github.com/morluto/rea/pull/4072) concern earlier loader replacement handling. The issue closure records its shared-ownership fix; the PR was closed as superseded. Its explicit unscopables-entry cases do not cover a quoted custom prototype setter. Closed [#2890](https://github.com/morluto/rea/issues/2890) and [#2892](https://github.com/morluto/rea/pull/2892) address local shadowing. Closed [#7418](https://github.com/morluto/rea/issues/7418) and merged [#7914](https://github.com/morluto/rea/pull/7914) address uncertain member writes to outer objects. This fixture instead loses a scalar loader assignment because its lookup is incorrectly classified as captured. Read-only calls inside a with body alone are not the decisive failure: both quoted and unquoted custom-prototype read-only controls remain unresolved. The reproduction above places a loader write inside with and its later call outside, exposing the concrete consequence without requiring exact resolution of the with-body read.
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#7795
No description provided.