[GH-ISSUE #5870] [Bug] contextBridge helper parameters lose nested API member paths #3558

Closed
opened 2026-10-31 00:41:50 +00:00 by bsakaguchi · 0 comments
Owner

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

Originally assigned to: @kaoru0822-kitauji on GitHub.

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 omits a bounded literal member path when the API object is passed through a helper parameter. For expose("desktop", { nested: { ping: ... } }), the API observation contains only ["nested"] and unknown_members: 0; summary.exposed_api_members is one. The same result contains a desktop.nested.ping exposure call path. Passing that same object directly to contextBridge.exposeInMainWorld retains ["nested", "nested.ping"] and counts two members.

Reproduction

From the repository root at the revision below, with the locked dependencies and compiled runtime already prepared, run this block. It creates disposable inputs, invokes the supported CLI, prints the relevant fields, and removes its inputs on exit. This block was executed successfully; setup/build commands were not rerun.

set -eu
repro_dir="$(mktemp -d)"
trap 'rm -rf "$repro_dir"' EXIT
mkdir -p "$repro_dir/helper" "$repro_dir/direct"
cat > "$repro_dir/helper/preload.js" <<'JS'
const { contextBridge, ipcRenderer } = require("electron");
function expose(key, api) { contextBridge.exposeInMainWorld(key, api); }
expose("desktop", { nested: { ping: () => ipcRenderer.send("ping") } });
JS
cat > "$repro_dir/direct/preload.js" <<'JS'
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("desktop", { nested: { ping: () => ipcRenderer.send("ping") } });
JS
for mode in helper direct; do
  node scripts/rea.mjs analyze-javascript-application "$repro_dir/$mode" --json > "$repro_dir/$mode.json"
done
node --input-type=module - "$repro_dir" <<'JS'
import { readFileSync } from "node:fs";
import { join } from "node:path";
for (const mode of ["helper", "direct"]) {
  const r = JSON.parse(readFileSync(join(process.argv[2], `${mode}.json`), "utf8")).normalized_result;
  const apis = r.graph.nodes.filter(n => n.kind === "context-bridge-api")
    .flatMap(n => n.observations.map(o => ({api_key: o.properties.api_key,
      members: o.properties.members, unknown_members: o.properties.unknown_members})));
  const entries = [...new Set(r.graph.nodes.flatMap(n => n.observations)
    .filter(o => o.properties.step_kind === "exposure")
    .map(o => o.properties.entry))].sort();
  console.log(JSON.stringify({mode, context_bridge_apis: r.summary.context_bridge_apis,
    exposed_api_members: r.summary.exposed_api_members, apis, exposure_entries: entries}));
}
JS

Expected behavior

The helper case should retain the same bounded literal paths ["nested", "nested.ping"] as the direct control. Both paths are within the supported nesting bound, and the nested function is already available to call-path reconstruction. The documented bridge-key/member recovery applies; the documented local wrapper substitution supports the helper call. The existing nested-member tests explicitly require dotted paths in this inventory. Any genuinely unresolved members should remain uncertain.

Impact and scope

P2: the exposed-member inventory and summary undercount the demonstrated helper-based API, even though its nested call path is present. Only one helper invocation is needed, with no spread, mutation, conditional selection or deep nesting. Analysis succeeds; no broader application failure was observed. A same-artifact automatic workaround returning the correct inventory was not established. The direct form is a diagnostic control that changes the source. Verification covered the built CLI on extracted directories; MCP transport and ASAR inputs were not replayed. No security boundary crossing was observed.

REA version

6.7.0, built source checkout at 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64. No Electron runtime or native analysis provider was used.

Target and artifact context

Synthetic extracted Electron application directories, each containing one CommonJS preload.js. REA parsed these files as inert source; the fixture code was not executed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed output from the block above (both analysis commands exited 0):

{"mode":"helper","context_bridge_apis":1,"exposed_api_members":1,"apis":[{"api_key":"desktop","members":["nested"],"unknown_members":0}],"exposure_entries":["desktop.nested.ping"]}
{"mode":"direct","context_bridge_apis":1,"exposed_api_members":2,"apis":[{"api_key":"desktop","members":["nested","nested.ping"],"unknown_members":0}],"exposure_entries":["desktop.nested.ping"]}

The helper result has resolution_basis: "wrapper-substitution", api_status: "dynamic", and partial graph coverage with no truncation. The direct control has resolution_basis: "direct-syntax" and complete graph coverage. The claim concerns specific retained key/member facts, not a claim that the helper graph advertises globally complete coverage.

Typed outcome or recovery expected

Return successful static analysis whose desktop API observation lists both nested and nested.ping and whose exposed-member count is two. Retain the existing desktop.nested.ping path and static-candidate provenance. For this fully resolved object, zero unknown members is consistent only with preserving the known bounded paths.

Analysis or suspected cause

Source-derived explanation: bridge finding construction and replacement uses resolver.members(object) and resolver.unknownMembers(object). Their object branches enumerate immediate keys and top-level uncertainty without descending into property values. The direct syntax collector recursively retains dotted paths, while bridge function traversal also visits nested objects. The merge with existing syntax members cannot help when the inner exposure argument is the helper parameter api. The graph projection and summary calculation consume the incomplete member array. No repair was applied or tested.

Additional context

Related closed #3212 concerned an inline object containing an unknown spread. Its fix at 0351328e54ce70ea3f2814482e76b132774aff48 preserves nested members already recovered by direct syntax. That fix is included in this baseline; it cannot supply nested paths absent from the inner helper call’s parameter. This case requires enumerating the substituted object, rather than only retaining direct syntax facts. Closed #2669 and merged PR #2745 concerned missing nested call paths, which this reproduction already retains.

Freshly fetched origin/main at f801b2fbfd28f4e3713adefddc7bd73db44c284c has no changes to the relevant producer, member resolvers, graph projection or summary files. Main was source-compared, not separately built. The loss of distinct repeated helper invocations is independent: this reproduction uses one invocation. No last known-good revision was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5870 Originally assigned to: @kaoru0822-kitauji on GitHub. ### 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` omits a bounded literal member path when the API object is passed through a helper parameter. For `expose("desktop", { nested: { ping: ... } })`, the API observation contains only `["nested"]` and `unknown_members: 0`; `summary.exposed_api_members` is one. The same result contains a `desktop.nested.ping` exposure call path. Passing that same object directly to `contextBridge.exposeInMainWorld` retains `["nested", "nested.ping"]` and counts two members. ### Reproduction From the repository root at the revision below, with the locked dependencies and compiled runtime already prepared, run this block. It creates disposable inputs, invokes the supported CLI, prints the relevant fields, and removes its inputs on exit. This block was executed successfully; setup/build commands were not rerun. ```sh set -eu repro_dir="$(mktemp -d)" trap 'rm -rf "$repro_dir"' EXIT mkdir -p "$repro_dir/helper" "$repro_dir/direct" cat > "$repro_dir/helper/preload.js" <<'JS' const { contextBridge, ipcRenderer } = require("electron"); function expose(key, api) { contextBridge.exposeInMainWorld(key, api); } expose("desktop", { nested: { ping: () => ipcRenderer.send("ping") } }); JS cat > "$repro_dir/direct/preload.js" <<'JS' const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("desktop", { nested: { ping: () => ipcRenderer.send("ping") } }); JS for mode in helper direct; do node scripts/rea.mjs analyze-javascript-application "$repro_dir/$mode" --json > "$repro_dir/$mode.json" done node --input-type=module - "$repro_dir" <<'JS' import { readFileSync } from "node:fs"; import { join } from "node:path"; for (const mode of ["helper", "direct"]) { const r = JSON.parse(readFileSync(join(process.argv[2], `${mode}.json`), "utf8")).normalized_result; const apis = r.graph.nodes.filter(n => n.kind === "context-bridge-api") .flatMap(n => n.observations.map(o => ({api_key: o.properties.api_key, members: o.properties.members, unknown_members: o.properties.unknown_members}))); const entries = [...new Set(r.graph.nodes.flatMap(n => n.observations) .filter(o => o.properties.step_kind === "exposure") .map(o => o.properties.entry))].sort(); console.log(JSON.stringify({mode, context_bridge_apis: r.summary.context_bridge_apis, exposed_api_members: r.summary.exposed_api_members, apis, exposure_entries: entries})); } JS ``` ### Expected behavior The helper case should retain the same bounded literal paths `["nested", "nested.ping"]` as the direct control. Both paths are within the supported nesting bound, and the nested function is already available to call-path reconstruction. The [documented bridge-key/member recovery](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/javascript-artifact-reconstruction.md#L253-L254) applies; the [documented local wrapper substitution](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/javascript-artifact-reconstruction.md#L370-L376) supports the helper call. The [existing nested-member tests](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/electronWrappedCallPaths.test.ts#L143-L175) explicitly require dotted paths in this inventory. Any genuinely unresolved members should remain uncertain. ### Impact and scope P2: the exposed-member inventory and summary undercount the demonstrated helper-based API, even though its nested call path is present. Only one helper invocation is needed, with no spread, mutation, conditional selection or deep nesting. Analysis succeeds; no broader application failure was observed. A same-artifact automatic workaround returning the correct inventory was not established. The direct form is a diagnostic control that changes the source. Verification covered the built CLI on extracted directories; MCP transport and ASAR inputs were not replayed. No security boundary crossing was observed. ### REA version 6.7.0, built source checkout at `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64. No Electron runtime or native analysis provider was used. ### Target and artifact context Synthetic extracted Electron application directories, each containing one CommonJS `preload.js`. REA parsed these files as inert source; the fixture code was not executed. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed output from the block above (both analysis commands exited 0): ```json {"mode":"helper","context_bridge_apis":1,"exposed_api_members":1,"apis":[{"api_key":"desktop","members":["nested"],"unknown_members":0}],"exposure_entries":["desktop.nested.ping"]} {"mode":"direct","context_bridge_apis":1,"exposed_api_members":2,"apis":[{"api_key":"desktop","members":["nested","nested.ping"],"unknown_members":0}],"exposure_entries":["desktop.nested.ping"]} ``` The helper result has `resolution_basis: "wrapper-substitution"`, `api_status: "dynamic"`, and partial graph coverage with no truncation. The direct control has `resolution_basis: "direct-syntax"` and complete graph coverage. The claim concerns specific retained key/member facts, not a claim that the helper graph advertises globally complete coverage. ### Typed outcome or recovery expected Return successful static analysis whose `desktop` API observation lists both `nested` and `nested.ping` and whose exposed-member count is two. Retain the existing `desktop.nested.ping` path and static-candidate provenance. For this fully resolved object, zero unknown members is consistent only with preserving the known bounded paths. ### Analysis or suspected cause Source-derived explanation: [bridge finding construction and replacement](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/electronWrappedCallPaths.ts#L522-L570) uses `resolver.members(object)` and `resolver.unknownMembers(object)`. Their [object branches](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/javascriptSymbolicBindings.ts#L436-L515) enumerate immediate keys and top-level uncertainty without descending into property values. The [direct syntax collector](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/electronStaticAnalysisValues.ts#L115-L147) recursively retains dotted paths, while [bridge function traversal](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/electronWrappedCallPaths.ts#L205-L235) also visits nested objects. The merge with existing syntax members cannot help when the inner exposure argument is the helper parameter `api`. The [graph projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/ElectronBoundaryGraphWindows.ts#L198-L239) and [summary calculation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/ElectronBoundaryAnalysis.ts#L129-L133) consume the incomplete member array. No repair was applied or tested. ### Additional context Related closed #3212 concerned an inline object containing an unknown spread. Its fix at `0351328e54ce70ea3f2814482e76b132774aff48` preserves nested members already recovered by direct syntax. That fix is included in this baseline; it cannot supply nested paths absent from the inner helper call’s parameter. This case requires enumerating the substituted object, rather than only retaining direct syntax facts. Closed #2669 and merged PR #2745 concerned missing nested call paths, which this reproduction already retains. Freshly fetched `origin/main` at `f801b2fbfd28f4e3713adefddc7bd73db44c284c` has no changes to the relevant producer, member resolvers, graph projection or summary files. Main was source-compared, not separately built. The loss of distinct repeated helper invocations is independent: this reproduction uses one invocation. No last known-good revision 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#3558
No description provided.