[GH-ISSUE #5586] [Bug] analyze-javascript-application counts one exposed IPC listener twice #3392

Closed
opened 2026-10-31 00:39:51 +00:00 by bsakaguchi · 0 comments
Owner

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

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 "$work_dir/fixture" --json succeeds but reports two renderer listeners and two pong channel entities for one literal ipcRenderer.on("pong", callback) call inside a contextBridge-exposed member. The IPC total is four instead of three. Passing the complete returned Evidence to trace-application-feature for the incoming pong channel succeeds with two seed matches.

An independent verifier reproduced both commands through the built CLI. This is static source analysis; Electron and the fixture were not executed.

Reproduction

  1. From a source checkout with dependencies installed and the CLI built, create this four-file fixture:
work_dir=$(mktemp -d)
mkdir -p "$work_dir/fixture"
printf '%s\n' '{"name":"ipc-probe","version":"1.0.0","main":"main.js"}' > "$work_dir/fixture/package.json"
cat > "$work_dir/fixture/main.js" <<'EOF'
const { app, BrowserWindow, ipcMain } = require("electron");

function createWindow() {
  const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } });
  window.loadFile("index.html");
}

ipcMain.handle("ping", () => "pong");
app.whenReady().then(createWindow);
EOF
cat > "$work_dir/fixture/preload.js" <<'EOF'
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("api", {
  ping: () => ipcRenderer.invoke("ping"),
  onPong: (callback) => ipcRenderer.on("pong", callback),
});
EOF
printf '%s\n' '<!doctype html><html><body></body></html>' > "$work_dir/fixture/index.html"
  1. Analyze and inspect the listener count and channel entities:
node scripts/rea.mjs analyze-javascript-application "$work_dir/fixture" --json > "$work_dir/application.json" 2> "$work_dir/analyze.stderr"
jq '.normalized_result.summary.ipc | {operations, renderer_listeners}' "$work_dir/application.json"
jq '[.normalized_result.graph.nodes[] | select(.kind == "ipc-channel" and any(.observations[]; .properties.channel == "pong")) | {node_id, identity, observations: [.observations[] | {properties, location: .evidence.location.value}]}]' "$work_dir/application.json"
  1. Trace the same literal channel using the saved complete Evidence:
jq -c '{application: ., seed: {kind: "channel", value: "pong"}, direction: "incoming"}' "$work_dir/application.json" > "$work_dir/trace-input.json"
node scripts/rea.mjs trace-application-feature "$work_dir/trace-input.json" --json > "$work_dir/trace.json" 2> "$work_dir/trace.stderr"
jq '.normalized_result | {matched_seeds: .summary.matched_seeds, seed_matches}' "$work_dir/trace.json"

These commands are the verifier's reduced fixture and CLI replay, with private workspace paths replaced by a temporary-directory variable. Verification isolated HOME and XDG state. Each reported analysis and trace completed successfully; repeated-run frequency was not measured.

Expected behavior

The fixture has three IPC call sites and one renderer listener. Analysis should report three operations, one renderer listener, and one pong channel entity; an exact incoming channel trace should match that one entity. Additional provenance should remain available without counting the same listener site twice.

docs/javascript-application-graph.md, “Stored record” and “Entity identity,” separates an entity from its observations. docs/javascript-artifact-reconstruction.md describes static Electron facts and wrapper substitution. src/domain/javascript/electronWrappedCallPaths.test.ts:372 keeps a directly observed renderer operation singular while tracing its exposed callback, using an invoke case. The direct-listener controls below establish the corresponding listener and shared-channel behavior.

Impact and scope

This bounded fixture silently overcounts static IPC operations and gives feature tracing a duplicate channel seed. Both commands still complete and retain source evidence. The verifier assessed impact as P2. Broader frequency and other platforms were not tested. Manual source inspection identifies the duplicate here; a generally safe automatic workaround was not verified. Deduplicating only by source range and channel could discard meaningful facts from distinct wrapper invocations.

REA version

rea-agents 6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node.js v26.12.0; macOS, arm64. No native analysis provider was used.

Target and artifact context

Local four-file Electron source fixture: package.json, main.js, preload.js, and index.html. No target execution or external provider calls.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The analysis and trace commands above both exited 0. Their jq projections establish:

{"operations":4,"renderer_listeners":2}

Two pong nodes refer to the same preload.js listener range, line 5, columns 24–56. One has observation-fingerprint identity scoped to electron-ipc-channel; the other has artifact-local-key identity in electron-inferred-ipc-channel, with an invocation location covering the bridge exposure. The trace reports matched_seeds: 2; both matches have kind ipc-channel.

The verifier also ran two controls, preserving the main-process handler in each:

  • Replacing the preload with one direct pong listener and no bridge gives operations=2, renderer_listeners=1, and one pong node with one observation.
  • Adding a second direct pong listener source site gives operations=3, renderer_listeners=2, and one pong node with two source observations.

Typed outcome or recovery expected

Successful analysis and tracing with the accurate counts and single channel entity above, while preserving the ping invoke-to-handler pairing and the exposed member's trace provenance. Two distinct direct listener sites must still count twice.

Analysis or suspected cause

Source-derived explanation: src/domain/javascript/electronWrappedCallPaths.ts:46–72,394–426 suppresses a traced copy of a directly observed send/invoke, but does not apply that suppression to a renderer listener. The direct listener and inferred copy have different invocation locations and both survive. src/application/javascript/ElectronBoundaryAnalysis.ts:134–144 counts those records, and src/application/javascript/ElectronBoundaryGraphIpc.ts:114–137 gives direct and inferred records different channel identities. These observations explain the CLI result; they do not establish runtime registration or dispatch.

Additional context

Prepared with AI assistance and independently reproduced through the production CLI, including the two direct-listener controls. The affected source files remain unchanged on freshly fetched origin/main at 080c0e2b2b82a7d9394b59d3d86630d9d85346a5. Searches included open and closed issues and PRs. Merged PRs #3779 and #3783 preserve the existing deduplication predicate and do not cover this listener case. Runtime listener-hook reports concern a different workflow.

NOT_ENOUGH_INFO for regression history: no last known-good release or first known-bad release was established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5586 ### 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 "$work_dir/fixture" --json` succeeds but reports two renderer listeners and two `pong` channel entities for one literal `ipcRenderer.on("pong", callback)` call inside a contextBridge-exposed member. The IPC total is four instead of three. Passing the complete returned Evidence to `trace-application-feature` for the incoming `pong` channel succeeds with two seed matches. An independent verifier reproduced both commands through the built CLI. This is static source analysis; Electron and the fixture were not executed. ### Reproduction 1. From a source checkout with dependencies installed and the CLI built, create this four-file fixture: ```sh work_dir=$(mktemp -d) mkdir -p "$work_dir/fixture" printf '%s\n' '{"name":"ipc-probe","version":"1.0.0","main":"main.js"}' > "$work_dir/fixture/package.json" cat > "$work_dir/fixture/main.js" <<'EOF' const { app, BrowserWindow, ipcMain } = require("electron"); function createWindow() { const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } }); window.loadFile("index.html"); } ipcMain.handle("ping", () => "pong"); app.whenReady().then(createWindow); EOF cat > "$work_dir/fixture/preload.js" <<'EOF' const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { ping: () => ipcRenderer.invoke("ping"), onPong: (callback) => ipcRenderer.on("pong", callback), }); EOF printf '%s\n' '<!doctype html><html><body></body></html>' > "$work_dir/fixture/index.html" ``` 2. Analyze and inspect the listener count and channel entities: ```sh node scripts/rea.mjs analyze-javascript-application "$work_dir/fixture" --json > "$work_dir/application.json" 2> "$work_dir/analyze.stderr" jq '.normalized_result.summary.ipc | {operations, renderer_listeners}' "$work_dir/application.json" jq '[.normalized_result.graph.nodes[] | select(.kind == "ipc-channel" and any(.observations[]; .properties.channel == "pong")) | {node_id, identity, observations: [.observations[] | {properties, location: .evidence.location.value}]}]' "$work_dir/application.json" ``` 3. Trace the same literal channel using the saved complete Evidence: ```sh jq -c '{application: ., seed: {kind: "channel", value: "pong"}, direction: "incoming"}' "$work_dir/application.json" > "$work_dir/trace-input.json" node scripts/rea.mjs trace-application-feature "$work_dir/trace-input.json" --json > "$work_dir/trace.json" 2> "$work_dir/trace.stderr" jq '.normalized_result | {matched_seeds: .summary.matched_seeds, seed_matches}' "$work_dir/trace.json" ``` These commands are the verifier's reduced fixture and CLI replay, with private workspace paths replaced by a temporary-directory variable. Verification isolated HOME and XDG state. Each reported analysis and trace completed successfully; repeated-run frequency was not measured. ### Expected behavior The fixture has three IPC call sites and one renderer listener. Analysis should report three operations, one renderer listener, and one `pong` channel entity; an exact incoming channel trace should match that one entity. Additional provenance should remain available without counting the same listener site twice. `docs/javascript-application-graph.md`, “Stored record” and “Entity identity,” separates an entity from its observations. `docs/javascript-artifact-reconstruction.md` describes static Electron facts and wrapper substitution. `src/domain/javascript/electronWrappedCallPaths.test.ts:372` keeps a directly observed renderer operation singular while tracing its exposed callback, using an invoke case. The direct-listener controls below establish the corresponding listener and shared-channel behavior. ### Impact and scope This bounded fixture silently overcounts static IPC operations and gives feature tracing a duplicate channel seed. Both commands still complete and retain source evidence. The verifier assessed impact as P2. Broader frequency and other platforms were not tested. Manual source inspection identifies the duplicate here; a generally safe automatic workaround was not verified. Deduplicating only by source range and channel could discard meaningful facts from distinct wrapper invocations. ### REA version rea-agents 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node.js v26.12.0; macOS, arm64. No native analysis provider was used. ### Target and artifact context Local four-file Electron source fixture: `package.json`, `main.js`, `preload.js`, and `index.html`. No target execution or external provider calls. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The analysis and trace commands above both exited 0. Their jq projections establish: ```json {"operations":4,"renderer_listeners":2} ``` Two `pong` nodes refer to the same `preload.js` listener range, line 5, columns 24–56. One has `observation-fingerprint` identity scoped to `electron-ipc-channel`; the other has `artifact-local-key` identity in `electron-inferred-ipc-channel`, with an invocation location covering the bridge exposure. The trace reports `matched_seeds: 2`; both matches have kind `ipc-channel`. The verifier also ran two controls, preserving the main-process handler in each: - Replacing the preload with one direct `pong` listener and no bridge gives operations=2, renderer_listeners=1, and one `pong` node with one observation. - Adding a second direct `pong` listener source site gives operations=3, renderer_listeners=2, and one `pong` node with two source observations. ### Typed outcome or recovery expected Successful analysis and tracing with the accurate counts and single channel entity above, while preserving the `ping` invoke-to-handler pairing and the exposed member's trace provenance. Two distinct direct listener sites must still count twice. ### Analysis or suspected cause Source-derived explanation: `src/domain/javascript/electronWrappedCallPaths.ts:46–72,394–426` suppresses a traced copy of a directly observed send/invoke, but does not apply that suppression to a renderer listener. The direct listener and inferred copy have different invocation locations and both survive. `src/application/javascript/ElectronBoundaryAnalysis.ts:134–144` counts those records, and `src/application/javascript/ElectronBoundaryGraphIpc.ts:114–137` gives direct and inferred records different channel identities. These observations explain the CLI result; they do not establish runtime registration or dispatch. ### Additional context Prepared with AI assistance and independently reproduced through the production CLI, including the two direct-listener controls. The affected source files remain unchanged on freshly fetched `origin/main` at `080c0e2b2b82a7d9394b59d3d86630d9d85346a5`. Searches included open and closed issues and PRs. Merged PRs #3779 and #3783 preserve the existing deduplication predicate and do not cover this listener case. Runtime listener-hook reports concern a different workflow. NOT_ENOUGH_INFO for regression history: no last known-good release or first known-bad release was established.
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#3392
No description provided.