[GH-ISSUE #5300] [Feature] Model webContents.ipc and the non-BrowserWindow window constructors in Electron static analysis #3231

Open
opened 2026-10-31 00:38:07 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @ppiova on GitHub (Oct 19, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5300

Current limitation

Electron static analysis recognizes a main-process IPC registration only when the receiver
object is literally named ipcMain, and recognizes a window only when the constructor path
is exactly BrowserWindow.

  • src/domain/javascript/electronStaticAnalysisIpc.ts:211 gates the main-process side on
    object === "ipcMain".
  • src/domain/javascript/electronStaticAnalysisBrowser.ts:46 matches a callee path of
    exactly ["BrowserWindow"].
  • src/domain/javascript/ contains no reference to WebContentsView, BaseWindow or
    BrowserView.

Two consequences follow. Registrations on webContents.ipc and webFrameMain.ipc, which
are IpcMain-shaped per-frame surfaces that never touch the global ipcMain, produce no
node. Window constructions through WebContentsView and BaseWindow produce no node
either, including when their webPreferences is a plain object literal that would
otherwise be fully resolvable.

The existing wrapper-following pass does not reach these cases. collectElectronWrappedCallPaths
seeds a path only from a bridge-member or an already recognized ipc-handler
(src/domain/javascript/electronWrappedCallPaths.ts:463), so a surface that is never
recognized cannot anchor an expansion.

Measured behavior on an open-source target

Measured with rea-agents@latest reporting rea@6.7.0, on VS Code 1.125.1, ground truth
built from microsoft/vscode pinned to the shipped product.json#commit and recounted
independently with an AST scan of the shipped bundle. Both denominators agree.

Category Ground truth Found False positives
BrowserWindow instantiations 3 3 0
Main-process IPC registrations, all layers 23 3 0
of which webContents.ipc.* 4 0 0
Distinct literal main-side channel names 11 0 0
webPreferences values resolved 26 0 0
Electron fuse bytes 9 9 0

No "vscode:" channel name appears anywhere in the 101 MB of returned Evidence. The three
IPC nodes that are returned are the on, once and handle methods of VS Code's own
ValidatedIpcMain class, all carrying resolution: "dynamic".

One miss is worth separating from the rest. The only place in the VS Code main process
where webPreferences keys and values are both statically resolvable is a
WebContentsView construction setting nodeIntegration: false, contextIsolation: true
and sandbox: true as literals. Those are literals REA could read today; they are missed
because the constructor is not modeled, not because the values are unresolvable.

To be clear about what this is not: nothing false was asserted in that run. Coverage was
partial throughout, unresolved channels were dynamic rather than absent, windows
carried absence_means_default: false, and the top-level limitation that missing findings
do not establish absence was present and accurate. There were zero false positives in
three runs.

The gap is in how the omission is declared. Where a surface is recognized, REA declares the
unknown at the place where the fact would have been, which lets a reader count what is
missing. Where the surface is not recognized at all, there is no node to carry a
declaration, and the omission is covered only by the blanket limitation. In this target
that is the difference between 3 declared unknowns and 20 undeclared absences.

Why this is the common shape, not an edge case

A census of four shipped main-process bundles on one Windows host, by AST scan rather than
grep. Counts of registration sites and channels are measured; the conclusion that REA would
not see them is derived from the source references above and was not measured against two
of the four.

Application On ipcMain On webContents.ipc
Zettlr 4.8.0 32 0
VS Code 1.125.1 3 direct, 16 through a wrapper 7
Closed-source app A (Rollup build) 5 0
Closed-source app B (Vite build, code-split main) 9 29, carrying 1,218 channels

The two closed-source applications are left unnamed deliberately. They are ordinary
desktop applications installed on the test host, the finding is about REA's coverage and
not about them, and nothing here depends on their identity.

Across the four, 1,796 channels are reachable only behind an application-owned layer
against 49 direct registration sites, and the largest single share sits on
webContents.ipc rather than behind a wrapper. Window constructors across the same four:
35 BrowserWindow, 13 WebContentsView, 1 BaseWindow, so 29 percent of construction
sites use a constructor that is not modeled.

Zettlr, which calls ipcMain.handle("channel", ...) directly, returned 32 of 32 in an
earlier run. It is the exception in this sample, not the rule.

Desired outcome and success criteria

  1. Classify an IPC registration when the receiver is a webContents.ipc or
    webFrameMain.ipc member, not only the global ipcMain. Where the owning
    webContents cannot be resolved to a window, the node should still exist and declare
    that as unknown rather than be dropped.
  2. Model WebContentsView, BaseWindow and BrowserView as window-like constructions, so
    a literal webPreferences on them is read the same way it is on BrowserWindow.
  3. Where an IPC-shaped call sits on a receiver that cannot be resolved to a known surface,
    emit a declared unknown rather than nothing, so pattern-coverage gaps are declared the
    way value-resolution gaps already are.

Success criterion on the reproducible target: on VS Code 1.125.1, the four
webContents.ipc registrations and the WebContentsView construction at
browserView.ts:114 appear as nodes, with the three literal webPreferences values
resolved. Zero false positives maintained.

Item 3 is the one with the broadest effect and is independent of the other two.

Expected interaction with targets

None beyond what the existing static pass already does. All of this is AST analysis of
shipped bundles. Nothing is executed.

Verification available so far

Measured on VS Code 1.125.1 with ground truth from the matching upstream commit, plus
three REA runs with full AST traversal and parse_failures: 0. Zettlr 4.8.0 served as the
control. Reproduction command, raw Evidence, ground truth with file and line, and the AST
scanners are available and can be attached if useful.

Not verified: REA was not run against the two closed-source applications in the census, so their
rows describe what the applications do and not a measured REA miss.

Alternatives considered

Extending wrapper following to seed from a wrapper definition rather than only from a
recognized endpoint. That was the first hypothesis, and the census argues against doing it
first: it would address the VS Code class pattern but would leave the larger
webContents.ipc share untouched.

Originally created by @ppiova on GitHub (Oct 19, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5300 ## Current limitation Electron static analysis recognizes a main-process IPC registration only when the receiver object is literally named `ipcMain`, and recognizes a window only when the constructor path is exactly `BrowserWindow`. - `src/domain/javascript/electronStaticAnalysisIpc.ts:211` gates the main-process side on `object === "ipcMain"`. - `src/domain/javascript/electronStaticAnalysisBrowser.ts:46` matches a callee path of exactly `["BrowserWindow"]`. - `src/domain/javascript/` contains no reference to `WebContentsView`, `BaseWindow` or `BrowserView`. Two consequences follow. Registrations on `webContents.ipc` and `webFrameMain.ipc`, which are `IpcMain`-shaped per-frame surfaces that never touch the global `ipcMain`, produce no node. Window constructions through `WebContentsView` and `BaseWindow` produce no node either, including when their `webPreferences` is a plain object literal that would otherwise be fully resolvable. The existing wrapper-following pass does not reach these cases. `collectElectronWrappedCallPaths` seeds a path only from a `bridge-member` or an already recognized `ipc-handler` (`src/domain/javascript/electronWrappedCallPaths.ts:463`), so a surface that is never recognized cannot anchor an expansion. ## Measured behavior on an open-source target Measured with `rea-agents@latest` reporting `rea@6.7.0`, on VS Code 1.125.1, ground truth built from `microsoft/vscode` pinned to the shipped `product.json#commit` and recounted independently with an AST scan of the shipped bundle. Both denominators agree. | Category | Ground truth | Found | False positives | | --- | --- | --- | --- | | BrowserWindow instantiations | 3 | 3 | 0 | | Main-process IPC registrations, all layers | 23 | 3 | 0 | | of which `webContents.ipc.*` | 4 | 0 | 0 | | Distinct literal main-side channel names | 11 | 0 | 0 | | `webPreferences` values resolved | 26 | 0 | 0 | | Electron fuse bytes | 9 | 9 | 0 | No `"vscode:"` channel name appears anywhere in the 101 MB of returned Evidence. The three IPC nodes that are returned are the `on`, `once` and `handle` methods of VS Code's own `ValidatedIpcMain` class, all carrying `resolution: "dynamic"`. One miss is worth separating from the rest. The only place in the VS Code main process where `webPreferences` keys and values are both statically resolvable is a `WebContentsView` construction setting `nodeIntegration: false`, `contextIsolation: true` and `sandbox: true` as literals. Those are literals REA could read today; they are missed because the constructor is not modeled, not because the values are unresolvable. To be clear about what this is not: nothing false was asserted in that run. Coverage was `partial` throughout, unresolved channels were `dynamic` rather than absent, windows carried `absence_means_default: false`, and the top-level limitation that missing findings do not establish absence was present and accurate. There were zero false positives in three runs. The gap is in how the omission is declared. Where a surface is recognized, REA declares the unknown at the place where the fact would have been, which lets a reader count what is missing. Where the surface is not recognized at all, there is no node to carry a declaration, and the omission is covered only by the blanket limitation. In this target that is the difference between 3 declared unknowns and 20 undeclared absences. ## Why this is the common shape, not an edge case A census of four shipped main-process bundles on one Windows host, by AST scan rather than grep. Counts of registration sites and channels are measured; the conclusion that REA would not see them is derived from the source references above and was not measured against two of the four. | Application | On `ipcMain` | On `webContents.ipc` | | --- | --- | --- | | Zettlr 4.8.0 | 32 | 0 | | VS Code 1.125.1 | 3 direct, 16 through a wrapper | 7 | | Closed-source app A (Rollup build) | 5 | 0 | | Closed-source app B (Vite build, code-split main) | 9 | 29, carrying 1,218 channels | The two closed-source applications are left unnamed deliberately. They are ordinary desktop applications installed on the test host, the finding is about REA's coverage and not about them, and nothing here depends on their identity. Across the four, 1,796 channels are reachable only behind an application-owned layer against 49 direct registration sites, and the largest single share sits on `webContents.ipc` rather than behind a wrapper. Window constructors across the same four: 35 `BrowserWindow`, 13 `WebContentsView`, 1 `BaseWindow`, so 29 percent of construction sites use a constructor that is not modeled. Zettlr, which calls `ipcMain.handle("channel", ...)` directly, returned 32 of 32 in an earlier run. It is the exception in this sample, not the rule. ## Desired outcome and success criteria 1. Classify an IPC registration when the receiver is a `webContents.ipc` or `webFrameMain.ipc` member, not only the global `ipcMain`. Where the owning `webContents` cannot be resolved to a window, the node should still exist and declare that as unknown rather than be dropped. 2. Model `WebContentsView`, `BaseWindow` and `BrowserView` as window-like constructions, so a literal `webPreferences` on them is read the same way it is on `BrowserWindow`. 3. Where an IPC-shaped call sits on a receiver that cannot be resolved to a known surface, emit a declared unknown rather than nothing, so pattern-coverage gaps are declared the way value-resolution gaps already are. Success criterion on the reproducible target: on VS Code 1.125.1, the four `webContents.ipc` registrations and the `WebContentsView` construction at `browserView.ts:114` appear as nodes, with the three literal `webPreferences` values resolved. Zero false positives maintained. Item 3 is the one with the broadest effect and is independent of the other two. ## Expected interaction with targets None beyond what the existing static pass already does. All of this is AST analysis of shipped bundles. Nothing is executed. ## Verification available so far Measured on VS Code 1.125.1 with ground truth from the matching upstream commit, plus three REA runs with full AST traversal and `parse_failures: 0`. Zettlr 4.8.0 served as the control. Reproduction command, raw Evidence, ground truth with file and line, and the AST scanners are available and can be attached if useful. Not verified: REA was not run against the two closed-source applications in the census, so their rows describe what the applications do and not a measured REA miss. ## Alternatives considered Extending wrapper following to seed from a wrapper definition rather than only from a recognized endpoint. That was the first hypothesis, and the census argues against doing it first: it would address the VS Code class pattern but would leave the larger `webContents.ipc` share untouched.
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#3231
No description provided.