[GH-ISSUE #4282] [Bug] Linux Electron capture cannot verify owned lineage: Chromium processes do not expose REA_PROCESS_RUN_ID in /proc/<pid>/environ #1695

Open
opened 2026-10-16 21:09:10 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @ethen-carter on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4282

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

On Linux (WSL2), capture-electron-scenario completed all three scenario actions and then failed with cleanup_incomplete (details.reason: cleanup_failed, cleanup_reason: "owned Electron lineage was unavailable; helper cleanup was not proven"). The capture is kept only in details.partial_observation with application.cleanup: "unverified".

Outside REA, I launched the same Electron 44.7.2 binary with REA_PROCESS_RUN_ID set, as ProviderProcess does, and read /proc/<pid>/environ for the launcher and all of its descendants. None of them exposed REA_PROCESS_RUN_ID, but the app's own process.env.REA_PROCESS_RUN_ID returned the value. The main and zygote environ ranges held only NUL bytes. The gpu, renderer, and utility ranges held command-line text. A control sleep process launched the same way exposed the variable.

Reproduction

REA run (reproduced 1/1):

  1. WSL2 Ubuntu 26.04, Node 24.22.0, rea-agents 6.5.0, Electron 44.7.2 linux-x64, WSLg display.
  2. rea capture-electron-scenario <scenario.json> --format json. The target is a minimal unpackaged local app with one window and one button. Actions: wait 1500 ms, then click #export, then wait 2500 ms.
  3. Exit code 1 with cleanup_incomplete, as shown below.

/proc probe without REA (reproduced 2/2, two different minimal apps):

  1. REA_PROCESS_RUN_ID=probe-e21 electron --user-data-dir=<tmp> main.js. main.js opens one window and writes process.env.REA_PROCESS_RUN_ID to a file.
  2. After 4 s, read /proc/<pid>/environ for the launcher and every descendant (pgrep -P, recursive).
  3. Control: REA_PROCESS_RUN_ID=probe-e21 sleep 5, then read its /proc/<pid>/environ.

Expected behavior

When REA launched the Electron app itself and the actions completed, REA should be able to verify the owned lineage and its cleanup on Linux. If that is not possible, the failure should say that the run token cannot be read from Chromium processes on this platform.

docs/electron-observation.md keeps the lane failed when cleanup is unverified. I am not asking for success while ownership is unproven. Unverified expectation: I did not find a statement of which platforms are expected to verify owned Electron cleanup.

Impact and scope

On this host, every owned Electron capture ends in cleanup_incomplete (1/1 REA run). Completed observations are therefore only available as partial_observation.

I did not check whether any processes remained after the failed run. Not tested: native (non-WSL) Linux, other Electron versions, Wayland or Xvfb instead of WSLg, and sandbox-related flags.

REA version

rea-agents 6.5.0 (npm). Source references below are from the 6.5.0 source archive.

Runtime and platform

Node 24.22.0; Ubuntu 26.04 LTS on WSL2 (kernel 6.18.33.2-microsoft-standard-WSL2), x64; Electron 44.7.2 linux-x64

Target and artifact context

Minimal unpackaged local Electron app (main.js plus one HTML page); not a binary target

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

REA result (redacted):

{
  "code": "cleanup_incomplete",
  "category": "execution_failure",
  "retryable": false,
  "details": {
    "reason": "cleanup_failed",
    "cleanup": "incomplete",
    "resources": ["owned_process_group", "run:<id>", "process-group:<pgid>"],
    "cleanup_reason": "owned Electron lineage was unavailable; helper cleanup was not proven",
    "execution_failure": "unreadable_output",
    "partial_observation": {
      "capture": {
        "application": { "electron_version": "44.7.2", "process_ownership": "provider-owned", "cleanup": "unverified" },
        "actions": ["wait-for-load: completed", "click-export: completed", "wait-after-export: completed"]
      }
    }
  }
}

Probe output (| = NUL byte, IDs redacted):

sleep pid=<p> has_REA_PROCESS_RUN_ID=1
pid=<p> (main)               environ_bytes=1989 has_REA_PROCESS_RUN_ID=0 head='||||||||||||||||'
pid=<p> --type=zygote        environ_bytes=2005 has_REA_PROCESS_RUN_ID=0 head='||||||||||||||||'
pid=<p> --type=gpu-process   environ_bytes=2005 has_REA_PROCESS_RUN_ID=0 head='m=x11 --enable-crash-reporter=<…>'
pid=<p> --type=renderer      environ_bytes=2045 has_REA_PROCESS_RUN_ID=0 head='r --enable-crash-reporter=<…>'
pid=<p> --type=utility       environ_bytes=2052 has_REA_PROCESS_RUN_ID=0 head='rack-uuid=<…>|||||'
app process.env: {"main_process_env":"probe-e21"}

Missing evidence: the REA result does not say which lineage check failed. The link between the probe and the REA failure is source-derived (see Analysis).

Typed outcome or recovery expected

Either cleanup is verified when the launched lineage is REA-owned, or the cleanup failure names the actual cause (the unreadable run token) instead of only the generic message.

Separately, execution_failure: "unreadable_output" does not describe a cleanup failure. In source, cleanup_failed is not mapped and falls through to the default at src/domain/analysisErrorProjection.ts:144. This is minor and can be split into its own issue.

Analysis or suspected cause

Source-derived (6.5.0):

  • src/process/ProviderProcess.ts:202-206 adds REA_PROCESS_RUN_ID to the launched child's environment.
  • On Linux, src/process/ProcessOwnershipObservation.ts:389-392 reads the token from /proc/<pid>/environ.
  • src/process/ProcessRunTokenObservations.ts:43-46 records a readable environment without the variable as runId: undefined.
  • observeOwnedProcessLineage (ProcessOwnershipObservation.ts:128-133) treats a run id that differs from the owned one as "unowned or PID-reused" and returns unavailable.
  • src/browser/PlaywrightElectronActiveProvider.ts:311-319: with unavailable lineage, POSIX cleanup returns cleaned: false with the reason above, even when the process-group cleanup itself succeeded.

Hypothesis, not checked against Chromium source here: Chromium's Linux process-title support reuses the original argv/environ memory and keeps the environment elsewhere. As a result, /proc/<pid>/environ no longer contains the launch environment. The NUL-filled main and zygote ranges and the command-line text in child ranges are consistent with this.

Additional context

  • Windows: the same scenario also fails closed on Windows (1/1, Electron 44.7.2, Node 24.16.0). This is expected since #1589, because the win32 process-table host returns [] (ProcessOwnershipObservation.ts:257-258). I am not reporting that as a bug here. docs/electron-observation.md does not say that owned Electron cleanup cannot be verified on Windows, though, so documenting it may help.
  • Related: #1589 (Windows unverified PID), #4227 (lifecycle contract audit), #3414 (owned Electron capture and X11 authorization; not involved here, because the app launched and all actions completed).
  • docs/process-capture.md already describes the same class of problem on macOS: changing a Node process's process.title overwrites argv memory and makes its run token unreadable. The Linux /proc/<pid>/environ reader may need a similar note or handling for Chromium-based children.
  • Workaround that produced verified evidence on the same host: launch Electron outside REA, then run only a CDP driver under capture-process on Linux. Cleanup verifies, because the only owned process is the driver.
  • Proposed checks (not run): repeat the probe on native Linux and under Xvfb; confirm that macOS is unaffected (it reads tokens through the Darwin reader, not /proc).

Probe script used:

REA_PROCESS_RUN_ID=probe-e21 "$ELECTRON" --user-data-dir="$(mktemp -d)" main.js >/dev/null 2>&1 &
ROOT=$!; sleep 4
collect() { echo "$1"; for c in $(pgrep -P "$1"); do collect "$c"; done; }
for p in $(collect "$ROOT"); do
  type=$(tr '\0' '\n' < /proc/$p/cmdline | grep -o -- '--type=[a-z-]*' | head -1)
  has=$(tr '\0' '\n' < /proc/$p/environ | grep -c '^REA_PROCESS_RUN_ID=')
  echo "pid=$p ${type:-(main)} has_REA_PROCESS_RUN_ID=$has"
done
Originally created by @ethen-carter on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4282 ### 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 Process capture or runtime observation ### Observed behavior On Linux (WSL2), `capture-electron-scenario` completed all three scenario actions and then failed with `cleanup_incomplete` (`details.reason: cleanup_failed`, `cleanup_reason: "owned Electron lineage was unavailable; helper cleanup was not proven"`). The capture is kept only in `details.partial_observation` with `application.cleanup: "unverified"`. Outside REA, I launched the same Electron 44.7.2 binary with `REA_PROCESS_RUN_ID` set, as `ProviderProcess` does, and read `/proc/<pid>/environ` for the launcher and all of its descendants. None of them exposed `REA_PROCESS_RUN_ID`, but the app's own `process.env.REA_PROCESS_RUN_ID` returned the value. The main and zygote `environ` ranges held only NUL bytes. The gpu, renderer, and utility ranges held command-line text. A control `sleep` process launched the same way exposed the variable. ### Reproduction REA run (reproduced 1/1): 1. WSL2 Ubuntu 26.04, Node 24.22.0, rea-agents 6.5.0, Electron 44.7.2 linux-x64, WSLg display. 2. `rea capture-electron-scenario <scenario.json> --format json`. The target is a minimal unpackaged local app with one window and one button. Actions: `wait 1500 ms`, then `click #export`, then `wait 2500 ms`. 3. Exit code 1 with `cleanup_incomplete`, as shown below. `/proc` probe without REA (reproduced 2/2, two different minimal apps): 1. `REA_PROCESS_RUN_ID=probe-e21 electron --user-data-dir=<tmp> main.js`. `main.js` opens one window and writes `process.env.REA_PROCESS_RUN_ID` to a file. 2. After 4 s, read `/proc/<pid>/environ` for the launcher and every descendant (`pgrep -P`, recursive). 3. Control: `REA_PROCESS_RUN_ID=probe-e21 sleep 5`, then read its `/proc/<pid>/environ`. ### Expected behavior When REA launched the Electron app itself and the actions completed, REA should be able to verify the owned lineage and its cleanup on Linux. If that is not possible, the failure should say that the run token cannot be read from Chromium processes on this platform. `docs/electron-observation.md` keeps the lane failed when cleanup is unverified. I am not asking for success while ownership is unproven. Unverified expectation: I did not find a statement of which platforms are expected to verify owned Electron cleanup. ### Impact and scope On this host, every owned Electron capture ends in `cleanup_incomplete` (1/1 REA run). Completed observations are therefore only available as `partial_observation`. I did not check whether any processes remained after the failed run. Not tested: native (non-WSL) Linux, other Electron versions, Wayland or Xvfb instead of WSLg, and sandbox-related flags. ### REA version rea-agents 6.5.0 (npm). Source references below are from the 6.5.0 source archive. ### Runtime and platform Node 24.22.0; Ubuntu 26.04 LTS on WSL2 (kernel 6.18.33.2-microsoft-standard-WSL2), x64; Electron 44.7.2 linux-x64 ### Target and artifact context Minimal unpackaged local Electron app (main.js plus one HTML page); not a binary target ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence REA result (redacted): ```json { "code": "cleanup_incomplete", "category": "execution_failure", "retryable": false, "details": { "reason": "cleanup_failed", "cleanup": "incomplete", "resources": ["owned_process_group", "run:<id>", "process-group:<pgid>"], "cleanup_reason": "owned Electron lineage was unavailable; helper cleanup was not proven", "execution_failure": "unreadable_output", "partial_observation": { "capture": { "application": { "electron_version": "44.7.2", "process_ownership": "provider-owned", "cleanup": "unverified" }, "actions": ["wait-for-load: completed", "click-export: completed", "wait-after-export: completed"] } } } } ``` Probe output (`|` = NUL byte, IDs redacted): ```text sleep pid=<p> has_REA_PROCESS_RUN_ID=1 pid=<p> (main) environ_bytes=1989 has_REA_PROCESS_RUN_ID=0 head='||||||||||||||||' pid=<p> --type=zygote environ_bytes=2005 has_REA_PROCESS_RUN_ID=0 head='||||||||||||||||' pid=<p> --type=gpu-process environ_bytes=2005 has_REA_PROCESS_RUN_ID=0 head='m=x11 --enable-crash-reporter=<…>' pid=<p> --type=renderer environ_bytes=2045 has_REA_PROCESS_RUN_ID=0 head='r --enable-crash-reporter=<…>' pid=<p> --type=utility environ_bytes=2052 has_REA_PROCESS_RUN_ID=0 head='rack-uuid=<…>|||||' app process.env: {"main_process_env":"probe-e21"} ``` Missing evidence: the REA result does not say which lineage check failed. The link between the probe and the REA failure is source-derived (see Analysis). ### Typed outcome or recovery expected Either cleanup is verified when the launched lineage is REA-owned, or the cleanup failure names the actual cause (the unreadable run token) instead of only the generic message. Separately, `execution_failure: "unreadable_output"` does not describe a cleanup failure. In source, `cleanup_failed` is not mapped and falls through to the default at `src/domain/analysisErrorProjection.ts:144`. This is minor and can be split into its own issue. ### Analysis or suspected cause Source-derived (6.5.0): - `src/process/ProviderProcess.ts:202-206` adds `REA_PROCESS_RUN_ID` to the launched child's environment. - On Linux, `src/process/ProcessOwnershipObservation.ts:389-392` reads the token from `/proc/<pid>/environ`. - `src/process/ProcessRunTokenObservations.ts:43-46` records a readable environment without the variable as `runId: undefined`. - `observeOwnedProcessLineage` (`ProcessOwnershipObservation.ts:128-133`) treats a run id that differs from the owned one as "unowned or PID-reused" and returns `unavailable`. - `src/browser/PlaywrightElectronActiveProvider.ts:311-319`: with unavailable lineage, POSIX cleanup returns `cleaned: false` with the reason above, even when the process-group cleanup itself succeeded. Hypothesis, not checked against Chromium source here: Chromium's Linux process-title support reuses the original argv/environ memory and keeps the environment elsewhere. As a result, `/proc/<pid>/environ` no longer contains the launch environment. The NUL-filled main and zygote ranges and the command-line text in child ranges are consistent with this. ### Additional context - Windows: the same scenario also fails closed on Windows (1/1, Electron 44.7.2, Node 24.16.0). This is expected since #1589, because the `win32` process-table host returns `[]` (`ProcessOwnershipObservation.ts:257-258`). I am not reporting that as a bug here. `docs/electron-observation.md` does not say that owned Electron cleanup cannot be verified on Windows, though, so documenting it may help. - Related: #1589 (Windows unverified PID), #4227 (lifecycle contract audit), #3414 (owned Electron capture and X11 authorization; not involved here, because the app launched and all actions completed). - `docs/process-capture.md` already describes the same class of problem on macOS: changing a Node process's `process.title` overwrites argv memory and makes its run token unreadable. The Linux `/proc/<pid>/environ` reader may need a similar note or handling for Chromium-based children. - Workaround that produced verified evidence on the same host: launch Electron outside REA, then run only a CDP driver under `capture-process` on Linux. Cleanup verifies, because the only owned process is the driver. - Proposed checks (not run): repeat the probe on native Linux and under Xvfb; confirm that macOS is unaffected (it reads tokens through the Darwin reader, not `/proc`). Probe script used: ```bash REA_PROCESS_RUN_ID=probe-e21 "$ELECTRON" --user-data-dir="$(mktemp -d)" main.js >/dev/null 2>&1 & ROOT=$!; sleep 4 collect() { echo "$1"; for c in $(pgrep -P "$1"); do collect "$c"; done; } for p in $(collect "$ROOT"); do type=$(tr '\0' '\n' < /proc/$p/cmdline | grep -o -- '--type=[a-z-]*' | head -1) has=$(tr '\0' '\n' < /proc/$p/environ | grep -c '^REA_PROCESS_RUN_ID=') echo "pid=$p ${type:-(main)} has_REA_PROCESS_RUN_ID=$has" done ```
Author
Owner

@N0zoM1z0 commented on GitHub (Oct 16, 2026):

Rechecked the producing ownership boundary on current main 13d04a0f19179a846e4516972a01b543dd389c48, native Linux under authenticated Xvfb, Node24.18.0 and official Electron44.7.0 (archive/executable hashes retained from the official release).

The source-owned app reports logical_run_token_present:true. Its live launcher /proc/<pid>/environ is readable (3,883bytes), entirely NUL, and contains no run marker. The actual observeOwnedProcessLineage returns:

{"status":"unavailable","reason":"process lineage contains an unowned or PID-reused process"}

A simultaneously launched sleep control returns verified, and survives the Electron observation. The app then self-quits through its fixture-owned stop file; no unknown process group is signalled. Final owned lineage is verified empty and a read-only scan finds no remaining process associated with the private profile. Whole probe2.95s, sampled family peak0.558GiB.

This is a direct production-boundary native reproduction, not a fresh capture-electron-scenario CLI/MCP run and not a runtime test of #4346. It avoids the separate XAUTHORITY propagation gap by passing the owned Xvfb environment directly to the fixture launcher. #4346's confirmed-exit settlement is related; this observation concerns a still-live Chromium launcher with a readable-but-overwritten environment. No alternate signaling authority or successful provider cleanup is inferred from PID/name/path/parentage alone.

Local source/probe/cleanup evidence: /tmp/rea-4282-latest-20261016/. The live-token ownership limitation remains distinct from the diagnostic correction already merged in #4406.

<!-- gh-comment-id:6219789964 --> @N0zoM1z0 commented on GitHub (Oct 16, 2026): Rechecked the producing ownership boundary on current main `13d04a0f19179a846e4516972a01b543dd389c48`, native Linux under authenticated Xvfb, Node24.18.0 and official Electron44.7.0 (archive/executable hashes retained from the official release). The source-owned app reports `logical_run_token_present:true`. Its live launcher `/proc/<pid>/environ` is readable (3,883bytes), entirely NUL, and contains no run marker. The actual `observeOwnedProcessLineage` returns: ```json {"status":"unavailable","reason":"process lineage contains an unowned or PID-reused process"} ``` A simultaneously launched sleep control returns `verified`, and survives the Electron observation. The app then self-quits through its fixture-owned stop file; no unknown process group is signalled. Final owned lineage is verified empty and a read-only scan finds no remaining process associated with the private profile. Whole probe2.95s, sampled family peak0.558GiB. This is a **direct production-boundary native reproduction**, not a fresh `capture-electron-scenario` CLI/MCP run and not a runtime test of #4346. It avoids the separate XAUTHORITY propagation gap by passing the owned Xvfb environment directly to the fixture launcher. #4346's confirmed-exit settlement is related; this observation concerns a still-live Chromium launcher with a readable-but-overwritten environment. No alternate signaling authority or successful provider cleanup is inferred from PID/name/path/parentage alone. Local source/probe/cleanup evidence: `/tmp/rea-4282-latest-20261016/`. The live-token ownership limitation remains distinct from the diagnostic correction already merged in #4406.
Author
Owner

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

Checked on Linux: part of this is fixed on main by 5a77acfbc. The commit's message says "Refs #4282", which is why GitHub left this issue open.

  • src/browser/PlaywrightElectronActiveProvider.test.ts and tests/conformance/contracts/analysisErrorProjection.test.ts pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (31 tests).
  • On the commit's parent, 9 of 17 fail, expected { code: 'cleanup_incomplete', ... } to match object.

The tests cover cleanup reasons. They do not cover the main ask, the owned lineage of a Linux Chromium process. So part of this may still be open.

Thanks, ethen-carter, for the clear report.

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

<!-- gh-comment-id:6254898199 --> @CassieFox23 commented on GitHub (Oct 18, 2026): Checked on Linux: part of this is fixed on main by 5a77acfbc. The commit's message says "Refs #4282", which is why GitHub left this issue open. - `src/browser/PlaywrightElectronActiveProvider.test.ts` and `tests/conformance/contracts/analysisErrorProjection.test.ts` pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (31 tests). - On the commit's parent, 9 of 17 fail, `expected { code: 'cleanup_incomplete', ... } to match object`. The tests cover cleanup reasons. They do not cover the main ask, the owned lineage of a Linux Chromium process. So part of this may still be open. Thanks, ethen-carter, for the clear report. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
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#1695
No description provided.