Skip to content

macOS sandbox blocks ps-backed recording process identity #2376

Open

Description

@janicduplessis

Purpose

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.
  • Detect changed start identity, zombie state, permission refusal, and exited processes.
  • 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.

Activity

  1. thymikee commented on Oct 9, 2026

    @thymikee
    Member

    Decision: one process-identity direction for this and #3291: move off ps to appandflow/unique-pid (#2377 was closed in its favor).

    Before switching packages/host-kit/src/internal/host-process.ts, check:

    • packaging on macOS, Linux and Windows (prebuilt binaries, install without a toolchain),
    • zombie processes and reused PIDs, plus the unknown and permission-denied cases, as typed outcomes,
    • the recording-owner guards still hold. An identity token doesn't make sending a signal atomic, so keep the existing guard order.

    #3291's Windows log-truncation failure is independent and shouldn't wait for this.

  2. andrea689 commented on Nov 11, 2026

    @andrea689

    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.

  3. thymikee commented on Nov 12, 2026

    @thymikee
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions