You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
macOS sandbox blocks ps-backed recording process identity #2376
Allow iOS simulator recording to establish and verify process ownership inside a macOS sandbox that refuses execution of /bin/ps.
Reproduction
Run a synthetic Node child-process identity query under sandbox-exec -p '(version 1)(allow default)'. On macOS where /bin/ps is setuid, its execution is refused. readProcessStartTime and readProcessCommand return null, and recording startup can fail with simctl recordVideo did not expose a complete process identity.
The shared host-process module currently shells out to ps. A non-privileged native probe using proc_pidinfo(PROC_PIDTBSDINFO) and sysctl(KERN_PROCARGS2) can read the same owned child under this policy. PROC_PIDTBSDINFO needs a nonzero argument to include zombie processes.
Required behavior
Keep command/start-time ownership checks and fail closed when identity cannot be proven; do not replace ownership with PID liveness alone.
Bound native reads and preserve argument boundaries before rendering the existing command string.
Package the native implementation without a global installation and keep non-macOS behavior unchanged.
Completion evidence
Real sandbox tests on synthetic owned children, malformed/bounded parsing tests, and iOS recording start/stop with a nonempty playable artifact. No benchmark transcripts or private run results are needed.
The same ps refusal now breaks the daemon itself under sandbox-exec, not only recording. Seen on 0.21.36 (macOS 27.2, Node 22); 0.21.19 worked under the same profile.
The client takes a daemon it spawned as ready only once daemon.json has a processStartTime equal to the one it reads itself, and both come from /bin/ps -o lstart=. Under the sandbox both are null, so every command stalls and fails:
$ sandbox-exec -p '(version 1)(allow default)' /bin/ps -o lstart= -p 1
sandbox-exec: execvp() of '/bin/ps' failed: Operation not permitted
$ AGENT_DEVICE_STATE_DIR=/tmp/ad-sbx sandbox-exec -p '(version 1)(allow default)' agent-device devices --platform ios
Error (COMMAND_FAILED): Failed to start daemon
Hint: The daemon is alive but made no startup progress for 60 s. The host can be overloaded. Retry once the host load drops; ...
# after 61 s
$ AGENT_DEVICE_STATE_DIR=/tmp/ad-sbx agent-device daemon stop
Error (COMMAND_FAILED): Daemon retirement could not be confirmed.
# the daemon keeps running
Two things made it slow to diagnose: the hint points at host load, and neither the error nor daemon.log says that ps was refused. Surfacing the EPERM there would help until the move to unique-pid lands.
A workaround that works for us with 0.21.36 under the sandbox, in case it helps someone meanwhile: a copy of /bin/ps re-signed ad hoc (cp /bin/ps <dir>/ps && codesign --force --sign - <dir>/ps; a plain copy is killed because of the original's entitlements), made outside the sandbox, plus NODE_OPTIONS=--require=<preload> with a preload that points child_process.spawn/spawnSync of /bin/ps at it. A profile that restricts writes under ~/.agent-device also has to allow the new <lock>.reclaim.lock guard that each lock release creates beside the runner cache's lock.
Triage: bug; the new report extends #2376 to daemon startup and retirement. On current main, isLaunchedDaemon requires a non-null start identity, while the shared process reader turns a refused ps execution into null. The existing unique-pid direction still applies; startup and stop must preserve ownership checks and expose unavailable identity evidence instead of diagnosing host load. Thanks for the version comparison and sandbox reproduction.
Activity
thymikee commentedon Oct 9, 2026
Decision: one process-identity direction for this and #3291: move off
pstoappandflow/unique-pid(#2377 was closed in its favor).Before switching
packages/host-kit/src/internal/host-process.ts, check:#3291's Windows log-truncation failure is independent and shouldn't wait for this.
andrea689 commentedon Nov 11, 2026
The same
psrefusal now breaks the daemon itself undersandbox-exec, not only recording. Seen on 0.21.36 (macOS 27.2, Node 22); 0.21.19 worked under the same profile.The client takes a daemon it spawned as ready only once
daemon.jsonhas aprocessStartTimeequal to the one it reads itself, and both come from/bin/ps -o lstart=. Under the sandbox both are null, so every command stalls and fails:Two things made it slow to diagnose: the hint points at host load, and neither the error nor
daemon.logsays thatpswas refused. Surfacing the EPERM there would help until the move to unique-pid lands.A workaround that works for us with 0.21.36 under the sandbox, in case it helps someone meanwhile: a copy of
/bin/psre-signed ad hoc (cp /bin/ps <dir>/ps && codesign --force --sign - <dir>/ps; a plain copy is killed because of the original's entitlements), made outside the sandbox, plusNODE_OPTIONS=--require=<preload>with a preload that pointschild_process.spawn/spawnSyncof/bin/psat it. A profile that restricts writes under~/.agent-devicealso has to allow the new<lock>.reclaim.lockguard that each lock release creates beside the runner cache's lock.thymikee commentedon Nov 12, 2026
Triage: bug; the new report extends #2376 to daemon startup and retirement. On current main,
isLaunchedDaemonrequires a non-null start identity, while the shared process reader turns a refusedpsexecution into null. The existing unique-pid direction still applies; startup and stop must preserve ownership checks and expose unavailable identity evidence instead of diagnosing host load. Thanks for the version comparison and sandbox reproduction.