Skip to content

bug: sx does not forward signals, orphaning IPC children on exit #37

@Pierozi

Description

@Pierozi

Context

When sx wraps a long-running command that spawns IPC-based children (e.g. Node processes using --useNodeIpc), the grandchildren get orphaned (PPID=1) when sx exits instead of being cleanly terminated. They accumulate across sessions and consume significant memory.

Process tree (real-world example: Claude Code with its TypeScript LSP plugin):

sx
 └── /usr/bin/sandbox-exec
      └── typescript-language-server (LSP wrapper)
           └── node tsserver.js --useNodeIpc
                └── node typingsInstaller.js

User Story

As a developer running an agentic tool (e.g. Claude Code, or any CLI that spawns LSP / language-server children) under sx, when my outer session ends, I expect every process the sandbox launched to be torn down — but Node IPC children are reparented to launchd and stay running forever, each holding 50–700 MB RSS.

Steps to Reproduce

  1. sx <profile> -- node -e 'setInterval(()=>{}, 1e9)' in one terminal. (Any long-running child works; IPC children expose it faster.)
  2. In another terminal, kill -TERM <sx-pid> (or SIGINT / SIGHUP).
  3. ps -eo pid,ppid,command | grep '[n]ode -e' — the Node child is still running, now with PPID=1.

Real-world reproduction: with the typescript-lsp Claude Code plugin enabled, a single day of sx claude sessions left 60+ orphaned tsserver.js / typingsInstaller.js processes totalling ~2.8 GB RSS.

Expected Behavior

When sx receives SIGINT/SIGTERM/SIGHUP (or exits for any reason), the entire sandboxed subtree receives SIGTERM and has a brief grace period before SIGKILL.

Actual Behavior

sx exits without signalling its child. sandbox-exec is reaped, the sandboxed grandchildren are reparented to PID 1, and nothing ever sends them a shutdown signal. Node children blocked on --useNodeIpc see their IPC peer vanish but (per a long-standing Node quirk) do not self-exit — they wait forever for a shutdown message on a closed channel.

Root Cause Investigation

  • src/sandbox/executor.rs:175cmd.spawn()?.wait()? with no signal forwarding setup.
  • No signal-hook, ctrlc, sigaction, or tokio::signal handler installed anywhere in src/ — confirmed by grep.
  • No setsid / setpgid / Command::process_group / pre_exec in src/ — the sandboxed child shares sx's process group, so normal shell SIGHUP-to-foreground-group delivery also doesn't reach it when sx is detached.
  • No Child::kill_on_drop(true) — a panic or early return in sx leaves the whole sandboxed subtree dangling.
  • src/sandbox/seatbelt.rs:138(allow signal (target self)) is orthogonal; it constrains outbound signals from sandboxed code, not inbound sx → child delivery.

Proposed Fix

  1. Launch the sandboxed child in its own process group: Command::process_group(0) (stable since Rust 1.64) or a pre_exec closure calling libc::setsid().
  2. Install a SIGINT/SIGTERM/SIGHUP handler (signal-hook or ctrlc) that, on signal, sends SIGTERM to -<child_pgid>, waits briefly (e.g. 2s), then escalates to SIGKILL.
  3. Set kill_on_drop(true) on the spawned Child for panic safety.

This is independent of the upstream Node --useNodeIpc behavior — forwarding a real SIGTERM kills tsserver cleanly and sidesteps the IPC-EOF quirk entirely.

Acceptance Criteria

  • sx forwards SIGINT/SIGTERM/SIGHUP to its sandboxed subtree.
  • The sandboxed child is launched in its own process group / session.
  • Child uses kill_on_drop(true).
  • Integration test: spawn a long-running process under sx, send SIGTERM to sx, assert zero descendants remain after 2 seconds (check via ps -eo ppid for absence).
  • Integration test: same as above with SIGKILL sent to sx (graceful propagation is impossible here; this documents the limit and kill_on_drop is not sufficient — note it as a known gap if so).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions