Repository navigation
fix(inspect): report Darwin topology for exiting processes - #23
Conversation
libproc returns ESRCH once a process starts exiting, while its kernel record still reports SRUN/SSTOP with P_WEXIT. The census only resolved zombies from that record, so exiting processes produced bare unknown rows. Accept kernel topology for zombies and P_WEXIT records; other ESRCH records stay unknown.
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex review: needs maintainer review before merge. What this changesThe Darwin census reports kernel-observed topology for exiting processes that libproc can no longer inspect. Example: Darwin returns libproc ESRCH while a retained process has kernel state SRUN and P_WEXIT set.
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review This PR fixes a source-confirmed Darwin census defect still present in main and v0.4.1; no actionable production correctness or security defect was found. Priority: P2 Before mergeNone. FindingsNone. Tests
Agent review detailsHow this fits togetherThe public inspection API passes census requests to native Rust, which reads Darwin process records and returns frozen JavaScript snapshots with independently observed topology and optionally verified identity. flowchart TD
A[Inspection caller] --> B[TypeScript census API]
B --> C[Rust Darwin census]
C --> D[libproc short records]
D --> E[Kernel record fallback]
E --> F[Birth identity checks]
F --> G[Process snapshots]
Technical reviewBest possible solution: Retain the focused extension of the existing kernel-record fallback while preserving unknown facts, denial handling, and identity brackets. Do we have a high-confidence way to reproduce the issue? Current-main source deterministically returns a bare row for ESRCH plus a non-zombie kernel record; the PR supplies a real-OS fixture and reports its before-and-after failure, which this review did not execute. Is this the best way to solve the issue? The change fixes the existing topology owner directly, using the kernel's explicit exiting flag without adding retries, application policy, public options, or a parallel inspection path. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 90b7b6b5b715. Provenance checked
TestingProof path: in-process harness. SecurityNone. EvidenceWhat I checked:
Review metrics
LabelsLabel changes:
Label justifications:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment Reviewed October 18, 2026, 7:15 PM ET / 23:15 UTC. |
A process inside XNU's exit window is hidden by libproc (ESRCH) while kern.proc.pid still shows it with P_WEXIT. proc-safe 0.4.1 returned a bare row for it, so the shared Darwin census owner failed every full census while any process on the host was mid-exit, and Codex app-server containment fell back to uncertain cleanup on busy Macs. Hold a setsid child in that window with an unread pty drain, verify the kernel window before and after inspection, and require readDarwinProcessTree and readDarwinProcessCensus to keep the child with its parent, group and running state. Red on proc-safe 0.4.1 (inspection unavailable); green with openclaw/proc-safe#23, which lands here through the 0.4.2 bump.
… processes exit (#176612) Pin @openclaw/proc-safe 0.4.2. Its Darwin census now reports parentage, process group, owner, and state for processes already exiting (openclaw/proc-safe#23) and omits rows proven gone by its trailing identity read (openclaw/proc-safe#25). OpenClaw's shared Darwin owner no longer fails a full census, and Codex app-server containment no longer reports cleanup "uncertain", whenever an unrelated process on the Mac is mid-exit. Adds an OpenClaw-boundary regression test that holds a child in XNU's exit window and requires readDarwinProcessTree and readDarwinProcessCensus to keep it with its topology. Release-age exclusions move to the 0.4.2 set (remove after 2026-10-26T00:51:31.079Z). Admin-merged over one inherited CI failure: run 39185036717 job 117845878737, an unhandled ERR_STREAM_WRITE_AFTER_END in the extensions/browser/src/browser/chrome-mcp.cleanup.test.ts fixture teardown. That test is unchanged by this PR. The failure reproduces on main with proc-safe 0.4.1 (5/5 runs at e2b6418, 3/6 at 9d013ba), and the job ran on Linux, where proc-safe 0.4.2 is unchanged.
… processes exit (openclaw#176612) Pin @openclaw/proc-safe 0.4.2. Its Darwin census now reports parentage, process group, owner, and state for processes already exiting (openclaw/proc-safe#23) and omits rows proven gone by its trailing identity read (openclaw/proc-safe#25). OpenClaw's shared Darwin owner no longer fails a full census, and Codex app-server containment no longer reports cleanup "uncertain", whenever an unrelated process on the Mac is mid-exit. Adds an OpenClaw-boundary regression test that holds a child in XNU's exit window and requires readDarwinProcessTree and readDarwinProcessCensus to keep it with its topology. Release-age exclusions move to the 0.4.2 set (remove after 2026-10-26T00:51:31.079Z). Admin-merged over one inherited CI failure: run 39185036717 job 117845878737, an unhandled ERR_STREAM_WRITE_AFTER_END in the extensions/browser/src/browser/chrome-mcp.cleanup.test.ts fixture teardown. That test is unchanged by this PR. The failure reproduces on main with proc-safe 0.4.1 (5/5 runs at 85afd2a, 3/6 at 11d9c97), and the job ran on Linux, where proc-safe 0.4.2 is unchanged.
Problem
On Darwin,
listProcesses()returns a bare row{ pid, owner: "unknown" }for a process that is in its exit window, whilereadProcessIdentity(pid)still returns a live identity (exited: false). Once XNU starts exiting a process, libproc'sproc_pidinfo(PROC_PIDT_SHORTBSDINFO)fails withESRCH, but the kernel record (sysctl KERN_PROC_PID) still describes it asSRUN/SSTOPwithP_WEXITset. The census already resolved thatESRCHfrom the kernel record for zombies, but returned unknown facts for every other state.Consumers that fail closed on unreadable topology (OpenClaw's shared Darwin census owner) therefore fail a whole census whenever any process on the host is mid-exit. On a loaded Mac Studio (load ~110), 31% of OpenClaw
readDarwinProcessTreecalls failed this way, and it is the remaining cause of intermittentcleanup: "uncertain"in OpenClaw's Codex app-server containment tests.Change
P_WEXIT.ESRCH, the census fills parent PID, process group, owner, and state from the kernel record when it is a zombie or exiting. Every otherESRCHrecord keeps the bare unknown row;EPERM/EACCESrows stay bare; kernel-record read errors still propagate. No newProcessState, no change toidentity.exited, Windows untouched.Evidence
Which ESRCH rows exist. A C probe ran full
proc_listallpids+ short-info censuses for 60 s at load ~110: 6,500 censuses / 11.7M rows. Every libproc-ESRCHrow whose kernel record still existed wasSZOMB(32,749, all withP_WEXIT) orSRUN+P_WEXIT(5,402). There were zeroESRCHrows that were neither, so gating onSZOMB || P_WEXITcovers the observed class without accepting unexplained records.Deterministic regression on the real OS boundary. The census fixture now holds its child inside the exit window. The child keeps a pty slave open with undrained output, and the fixture never reads the master. XNU's last-close drain holds the child at libproc
ESRCH+SRUN+P_WEXITfor a stable ~600 ms before it becomes a zombie. The fixture verifies that exact kernel condition in C before printingexiting, fails loudly if the window is missed, and re-checks the window after the proof's selected and full censuses, so a late sample cannot pass. The new stage runs on the main thread, because a Worker spawn does not fit the window reliably under load. The existing running/stopped/zombie/gone stages keep main + Worker coverage.real Darwin census and Worker boundariesfails withexiting row must retain its parent PID(actualundefined), with selected and full rows{"pid":…,"owner":"unknown"}, while the fixture confirmsstillExiting: true,libprocErrno: 3,state: 2,flags: 0x2084.PROC_SAFE_NATIVE=require pnpm test test/inspect.test.tspassed 24/24 three consecutive times at load ~110. The standalone proof reportspassed: 9, with censuses completing ~12 ms into a ~601 ms window.Gates (macOS arm64, load ~100–125):
PROC_SAFE_NATIVE=require pnpm checkpassed (104 passed, 8 skipped, pack check OK);cargo clippy --workspace --locked -- -D warningsclean;cargo test --workspace --lockedpassed;pnpm package:smokepassed host-only (npm and pnpm root-only installs, main and Worker);git diff --checkclean. One run of the unrelatedtest/native-status.test.tshit its 5 s timeout under this load and passed on rerun.Downstream impact (OpenClaw owner
readDarwinProcessTree, same owner code, 2,000 censuses each).unavailable)unavailable, 5permission)permission)The residual
permissionclass exists before and after this change. A selected-lineage process gets reaped between the census's topology read and its trailing birth-identity bracket, so the row keeps topology without identity. It is tracked separately.Not included
No version bump, tag, or release; releasing needs maintainer approval.