[GH-ISSUE #3196] [Bug] Electron bridge paths lose conditions from preceding early returns #1134

Closed
opened 2026-10-15 18:31:22 +00:00 by bsakaguchi · 2 comments
Owner

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

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

node scripts/rea.mjs analyze-javascript-application <fixture-directory> --format json drops a conditional early-return predicate from a recovered Electron bridge-member path. For an exposure after if(x)return;, the returned api.copy terminal candidate has an empty condition list. Its source parse status, path evidence coverage and application graph coverage are all complete.

The equivalent exposure inside if(!x){...} retains !x and reports partial for those coverage fields. An independent verifier observed this difference through the built CLI with four inert CommonJS fixtures. All four analyses exited successfully. The candidate retains its static-candidate designation and runtime limitations; the incorrect result concerns its missing source predicate and overstated static coverage.

Reproduction

  1. In a clean checkout of 58300885207133422af235e0f2b10658cf958b03, use the repository's pinned development toolchain and prepare the CLI:
npm ci
npm run build:cached
  1. From the repository root, choose an absent repro-work directory, isolate the CLI's home and configuration directories, create four local fixtures, and analyze each fixture. The Python program invokes the supported CLI and prints only the relevant graph properties. It does not execute the fixture JavaScript.
mkdir -p repro-work/home
export HOME="$PWD/repro-work/home"
export XDG_CONFIG_HOME="$HOME/config"
export XDG_CACHE_HOME="$HOME/cache"
export XDG_DATA_HOME="$HOME/data"
python3 - <<'PYREPRO'
import json
import os
import subprocess
from pathlib import Path

root = Path(os.environ["HOME"]).parent
exposure = 'e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});'
bodies = {
    "early": "if(x)return;" + exposure,
    "nested": "if(!x){" + exposure + "}",
    "dead": "return;" + exposure,
    "unguarded": exposure,
}
for name, body in bodies.items():
    app = root / name
    app.mkdir()
    (app / "package.json").write_text('{"name":"guard-repro","version":"1.0.0","main":"index.js"}\n')
    (app / "index.js").write_text('const e=require("electron");function f(x){' + body + '}f(globalThis.blocked);\n')
    run = subprocess.run(
        ["node", "scripts/rea.mjs", "analyze-javascript-application", str(app), "--format", "json"],
        check=True, text=True, capture_output=True,
    )
    graph = json.loads(run.stdout)["normalized_result"]["graph"]
    observations = [o for node in graph["nodes"] for o in node["observations"]]
    paths = [o for o in observations if o["properties"].get("entry") == "api.copy"
             and o["properties"].get("step_kind") == "terminal"]
    source = next(o for o in observations if o["properties"].get("path") == "index.js"
                  and o["properties"].get("file_kind") == "javascript")
    print(json.dumps({
        "fixture": name,
        "parse_status": source["properties"]["parse_status"],
        "graph_coverage": graph["coverage"]["status"],
        "terminal_paths": [{"entry": o["properties"]["entry"],
                            "terminal": o["properties"]["terminal"],
                            "conditions": [c["expression"] for c in o["properties"]["conditions"]],
                            "coverage": o["evidence"]["coverage"]["status"]} for o in paths],
    }))
PYREPRO
  1. Compare the printed summaries. The early-return and nested fixtures both expose api.copy only when the unknown runtime parameter x is falsy. The early fixture reports empty conditions and complete coverage; the nested fixture retains !x and partial coverage. The definite-return fixture has no terminal path, and the unguarded fixture has empty conditions and complete coverage.

The verifier replayed this fixture program and observed all four summaries below. Verification used prepared dependencies and compiled runtime artifacts; it did not rerun npm ci or npm run build:cached.

Expected behavior

The early-return api.copy candidate should retain !(x) with its source location and the uncertainty associated with that condition. Its source parse status, path evidence coverage and application graph coverage should be partial, consistent with the equivalent nested fixture. The definite-return fixture should continue to have no recovered api.copy terminal path, and the unguarded fixture should retain empty conditions and complete application graph coverage.

docs/javascript-artifact-reconstruction.md:312-327 documents recovered Electron paths that retain source ranges and branch conditions as static candidates. It explicitly says the pass neither executes the bundle nor proves that runtime guards pass. Predicate retention is the expected contract here.

Impact and scope

A bounded set of conditional Electron seed paths loses meaningful source branch evidence and appears unguarded with complete static coverage. The equivalent enclosing branch and definite-return controls work. The separate semantic graph has its own coverage and uncertainty; this report concerns source parse status, recovered path evidence and the application graph.

No runtime execution, access-control bypass or security boundary crossing was observed. Population frequency and other early-exit variants were not measured. No supported analysis option restoring the predicate was demonstrated. Rewriting the artifact into a nested branch changes its evidence identity, so a practical workaround remains unverified.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node.js 26.10.0; macOS arm64. The verifier recorded Darwin arm64 but did not record an OS release number.

Target and artifact context

Local synthetic CommonJS application directories, each containing a package record and one index.js. The function parameter receives an unknown runtime value through globalThis.blocked. REA parses these inputs without executing their JavaScript. Electron and native analysis engines are not required.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

{"fixture":"early","parse_status":"complete","graph_coverage":"complete","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":[],"coverage":"complete"}]}
{"fixture":"nested","parse_status":"partial","graph_coverage":"partial","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":["!x"],"coverage":"partial"}]}
{"fixture":"dead","parse_status":"complete","graph_coverage":"complete","terminal_paths":[]}
{"fixture":"unguarded","parse_status":"complete","graph_coverage":"complete","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":[],"coverage":"complete"}]}

Typed outcome or recovery expected

A successful analysis should preserve !(x) and its location in normalized_result.graph.nodes[*].observations[*].properties.conditions for the api.copy terminal step. The source observation's parse_status, terminal evidence coverage and normalized_result.graph.coverage.status should be partial. The result should continue to identify the path as a static candidate without claiming that its guard passed at runtime.

Analysis or suspected cause

Source-derived explanation at the stated baseline:

  • src/domain/javascript/electronWrappedCallPaths.ts:608-645 retains enclosing branch predicates during seed discovery and checks preceding block siblings for definite exits, but does not retain the negative predicate of a preceding conditional return.
  • src/domain/javascript/electronWrappedCallPaths.ts:528-545 already retains negative early-rejection predicates during callback/block traversal. The callback regression in src/domain/javascript/electronWrappedCallPaths.test.ts:577-603 covers that traversal rather than seed discovery.
  • src/application/javascript/ElectronBoundaryGraphCallPaths.ts:31-77 publishes recovered path conditions on graph observations.
  • src/application/javascript/JavaScriptArtifactGraphContext.ts:263-273 derives file evidence coverage from source parse status.
  • src/domain/javascript/electronWrappedCallPaths.test.ts:476-505 requires enclosing predicates and partial parse status on recovered paths.

Additional context

Related closed issue #2668 concerns conditions lost only when projecting IPC join edges, while this defect loses the predicate during seed discovery before any join exists.

P2 is supported because the verified fixtures show incorrect static evidence for a bounded input shape while the other controls remain analyzable. No broader workflow failure or runtime security consequence was established. Read-only dedupe inspection found the identified failure files unchanged between the verified baseline and fetched main at 767fe0f0ccbab3ae4be28e71cc8214c7cd302133.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3196 ### 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 `node scripts/rea.mjs analyze-javascript-application <fixture-directory> --format json` drops a conditional early-return predicate from a recovered Electron bridge-member path. For an exposure after `if(x)return;`, the returned `api.copy` terminal candidate has an empty condition list. Its source parse status, path evidence coverage and application graph coverage are all `complete`. The equivalent exposure inside `if(!x){...}` retains `!x` and reports `partial` for those coverage fields. An independent verifier observed this difference through the built CLI with four inert CommonJS fixtures. All four analyses exited successfully. The candidate retains its static-candidate designation and runtime limitations; the incorrect result concerns its missing source predicate and overstated static coverage. ### Reproduction 1. In a clean checkout of `58300885207133422af235e0f2b10658cf958b03`, use the repository's pinned development toolchain and prepare the CLI: ```sh npm ci npm run build:cached ``` 2. From the repository root, choose an absent `repro-work` directory, isolate the CLI's home and configuration directories, create four local fixtures, and analyze each fixture. The Python program invokes the supported CLI and prints only the relevant graph properties. It does not execute the fixture JavaScript. ```sh mkdir -p repro-work/home export HOME="$PWD/repro-work/home" export XDG_CONFIG_HOME="$HOME/config" export XDG_CACHE_HOME="$HOME/cache" export XDG_DATA_HOME="$HOME/data" python3 - <<'PYREPRO' import json import os import subprocess from pathlib import Path root = Path(os.environ["HOME"]).parent exposure = 'e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});' bodies = { "early": "if(x)return;" + exposure, "nested": "if(!x){" + exposure + "}", "dead": "return;" + exposure, "unguarded": exposure, } for name, body in bodies.items(): app = root / name app.mkdir() (app / "package.json").write_text('{"name":"guard-repro","version":"1.0.0","main":"index.js"}\n') (app / "index.js").write_text('const e=require("electron");function f(x){' + body + '}f(globalThis.blocked);\n') run = subprocess.run( ["node", "scripts/rea.mjs", "analyze-javascript-application", str(app), "--format", "json"], check=True, text=True, capture_output=True, ) graph = json.loads(run.stdout)["normalized_result"]["graph"] observations = [o for node in graph["nodes"] for o in node["observations"]] paths = [o for o in observations if o["properties"].get("entry") == "api.copy" and o["properties"].get("step_kind") == "terminal"] source = next(o for o in observations if o["properties"].get("path") == "index.js" and o["properties"].get("file_kind") == "javascript") print(json.dumps({ "fixture": name, "parse_status": source["properties"]["parse_status"], "graph_coverage": graph["coverage"]["status"], "terminal_paths": [{"entry": o["properties"]["entry"], "terminal": o["properties"]["terminal"], "conditions": [c["expression"] for c in o["properties"]["conditions"]], "coverage": o["evidence"]["coverage"]["status"]} for o in paths], })) PYREPRO ``` 3. Compare the printed summaries. The early-return and nested fixtures both expose `api.copy` only when the unknown runtime parameter `x` is falsy. The early fixture reports empty conditions and complete coverage; the nested fixture retains `!x` and partial coverage. The definite-return fixture has no terminal path, and the unguarded fixture has empty conditions and complete coverage. The verifier replayed this fixture program and observed all four summaries below. Verification used prepared dependencies and compiled runtime artifacts; it did not rerun `npm ci` or `npm run build:cached`. ### Expected behavior The early-return `api.copy` candidate should retain `!(x)` with its source location and the uncertainty associated with that condition. Its source parse status, path evidence coverage and application graph coverage should be `partial`, consistent with the equivalent nested fixture. The definite-return fixture should continue to have no recovered `api.copy` terminal path, and the unguarded fixture should retain empty conditions and complete application graph coverage. `docs/javascript-artifact-reconstruction.md:312-327` documents recovered Electron paths that retain source ranges and branch conditions as static candidates. It explicitly says the pass neither executes the bundle nor proves that runtime guards pass. Predicate retention is the expected contract here. ### Impact and scope A bounded set of conditional Electron seed paths loses meaningful source branch evidence and appears unguarded with complete static coverage. The equivalent enclosing branch and definite-return controls work. The separate semantic graph has its own coverage and uncertainty; this report concerns source parse status, recovered path evidence and the application graph. No runtime execution, access-control bypass or security boundary crossing was observed. Population frequency and other early-exit variants were not measured. No supported analysis option restoring the predicate was demonstrated. Rewriting the artifact into a nested branch changes its evidence identity, so a practical workaround remains unverified. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node.js 26.10.0; macOS arm64. The verifier recorded Darwin arm64 but did not record an OS release number. ### Target and artifact context Local synthetic CommonJS application directories, each containing a package record and one `index.js`. The function parameter receives an unknown runtime value through `globalThis.blocked`. REA parses these inputs without executing their JavaScript. Electron and native analysis engines are not required. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence ```text {"fixture":"early","parse_status":"complete","graph_coverage":"complete","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":[],"coverage":"complete"}]} {"fixture":"nested","parse_status":"partial","graph_coverage":"partial","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":["!x"],"coverage":"partial"}]} {"fixture":"dead","parse_status":"complete","graph_coverage":"complete","terminal_paths":[]} {"fixture":"unguarded","parse_status":"complete","graph_coverage":"complete","terminal_paths":[{"entry":"api.copy","terminal":"electron.ipcRenderer.send","conditions":[],"coverage":"complete"}]} ``` ### Typed outcome or recovery expected A successful analysis should preserve `!(x)` and its location in `normalized_result.graph.nodes[*].observations[*].properties.conditions` for the `api.copy` terminal step. The source observation's `parse_status`, terminal evidence coverage and `normalized_result.graph.coverage.status` should be `partial`. The result should continue to identify the path as a static candidate without claiming that its guard passed at runtime. ### Analysis or suspected cause Source-derived explanation at the stated baseline: - `src/domain/javascript/electronWrappedCallPaths.ts:608-645` retains enclosing branch predicates during seed discovery and checks preceding block siblings for definite exits, but does not retain the negative predicate of a preceding conditional return. - `src/domain/javascript/electronWrappedCallPaths.ts:528-545` already retains negative early-rejection predicates during callback/block traversal. The callback regression in `src/domain/javascript/electronWrappedCallPaths.test.ts:577-603` covers that traversal rather than seed discovery. - `src/application/javascript/ElectronBoundaryGraphCallPaths.ts:31-77` publishes recovered path conditions on graph observations. - `src/application/javascript/JavaScriptArtifactGraphContext.ts:263-273` derives file evidence coverage from source parse status. - `src/domain/javascript/electronWrappedCallPaths.test.ts:476-505` requires enclosing predicates and partial parse status on recovered paths. ### Additional context Related closed issue [#2668](https://github.com/morluto/rea/issues/2668) concerns conditions lost only when projecting IPC join edges, while this defect loses the predicate during seed discovery before any join exists. P2 is supported because the verified fixtures show incorrect static evidence for a bounded input shape while the other controls remain analyzable. No broader workflow failure or runtime security consequence was established. Read-only dedupe inspection found the identified failure files unchanged between the verified baseline and fetched main at `767fe0f0ccbab3ae4be28e71cc8214c7cd302133`.
bsakaguchi 2026-10-15 18:31:22 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

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

Heads up: this looks fixed on main in 503109995. The change is in src/domain/javascript/electronWrappedCallPaths.ts, and preserves early-return predicates on recovered bridge paths through CLI and stdio MCP covers it. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list.

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

<!-- gh-comment-id:6174998565 --> @CassieFox23 commented on GitHub (Oct 14, 2026): Heads up: this looks fixed on main in 503109995. The change is in `src/domain/javascript/electronWrappedCallPaths.ts`, and `preserves early-return predicates on recovered bridge paths through CLI and stdio MCP` covers it. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Resolved on main by 503109995.

Conditions from preceding definite early returns now propagate into the later Electron bridge call paths.

Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: tests/boundary/cli/electronWrappedCallPaths.test.ts.

<!-- gh-comment-id:6175068831 --> @morluto commented on GitHub (Oct 14, 2026): Resolved on main by 503109995. Conditions from preceding definite early returns now propagate into the later Electron bridge call paths. Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: tests/boundary/cli/electronWrappedCallPaths.test.ts.
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#1134
No description provided.