Repository navigation
runner: end a sandboxed pipe process with its whole sandbox, also during setup (#109) - #118
Merged
Merged
Conversation
On Linux, terminating a sandboxed pipe process, or its timeout, killed the sandbox helper alone. bubblewrap's sandbox process sets its parent-death signal only just before it runs the command, so a kill during the sandbox's setup left that process running under PID 1 with the command, which kept the Runner's output pipes open. A sandboxed pipe root is now stopped first, then each process below a stopped one, read from /proc a level at a time, and every process found is killed, children before their parents, before the root. Killing the sandbox's first process ends its PID namespace. Other roots are killed alone, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Closes #109
mainat0ac31915c6f7776e7e065; both unchangedSummary
Before: on Linux, terminating a sandboxed pipe process, or its timeout, killed the sandbox helper alone. bubblewrap's sandbox process sets its parent-death signal only just before it runs the command, so a kill that landed while the sandbox was being set up left that process running under PID 1. It then ran the command, which kept the Runner's output pipes open: the Runner reported the end only when the command ended on its own, and a second terminate did nothing, as the Runner had already reaped the helper.
After: the Runner ends a sandboxed pipe process with every process it started, also during the sandbox's setup. A terminate or a timeout first stops the helper, then each process below a stopped one, from the top down, then kills them all, children before their parents, and the helper last, as before. Killing the sandbox's first process ends its PID namespace, so everything in the sandbox ends with it.
How.
crates/runner/src/sandbox_tree.rs(new, Linux only):kill_descendantsstops the root with SIGSTOP, waits until each of its threads has stopped, then reads/procand stops the processes whose parent it has already stopped, a level at a time, until a read finds none. Each read comes after every process found so far has stopped, so none can start a process or reap one: each process ID it signals stays that process's own. It then sends SIGKILL to each process it found, in reverse order. SIGSTOP and SIGKILL reach a PID namespace's first process from the Runner's namespace whatever signal handlers it has set. The waits share a 500 ms budget, after which the walk goes on without waiting. It never signals PID 0, a negative PID or the Runner itself, and does nothing when/procdoes not show the root as the Runner's child.crates/runner/src/process.rs: a pipe slot records whether the Linux sandbox launched it, andrequest_kill(terminate, workspace termination and the independent Runner's stop) and the timeout kill a pipe root throughkill_pipe_root, which walks a sandboxed root's processes first. Any other root is killed alone, as before.docs/runner-isolation.mdand its Korean pair: the paragraph on ending a process says how a sandboxed pipe process ends. The pair's hashes are recorded withscripts/check_docs.py record.Contract changes
None to tool schemas, error codes, status values or IDs. Ending a sandboxed pipe process now ends its sandbox also while the sandbox is being set up, as it already did once the command ran, through bubblewrap's parent-death signal. Processes that the Linux sandbox does not run, and
tty: trueprocesses, end as before.Tests
Verified here on the same change at
5b4824a, based ond0f2e49, before it was rebased onto0ac3191, which changes only scripts and documentation; in a Linux cloud container running as root, with bubblewrap 0.9.0, the Codex checkout with its recorded patch applied, and the Linux sandbox helper built:/bin/sleeprunning.process::tests::terminate_during_sandbox_setup_leaves_no_process_behind(new) starts a sandboxed/bin/sleep, waits until the sandbox's first process exists and still runs bubblewrap, under the bubblewrap that runs the command, then terminates it, and requires the output to end within 2 s with nosleepleft. It repeats until 5 terminates landed during setup. It failed 3 of 3 runs withkill_pipe_root's walk disabled, at the first or second terminate, and passed 25 of 25 runs with it.sandbox_tree::tests(new): a root whose shell startssleepin a session of its own, which outlives a kill of the root alone, ends with every process below it while the root stays stopped; a process that is not the caller's child is left alone; PID 0, an out-of-range PID and the caller are never signalled; andstatlines with spaces and parentheses in the command name parse.cargo test --locked -p codespace-runner --lib, with the sandbox helper available: 158 passed.cargo fmt --check;cargo clippy --locked --all-targets -- -D warnings, and-p codespace-runner --features devguard.python3 scripts/check_docs.py check(22 paired documents) andscripts/check-no-model-deps.sh.Not verified here: the workspace's other test suites and the macOS checks, which CI runs.
Security scenarios
killwould take as a process group or every process, and the Runner's own PID.Out of scope
tty: truesandboxed processes. Their termination signals the root's process group, which bubblewrap's sandbox process leaves only just before it sets its parent-death signal, so the window is much narrower but not closed. The pinned Codex PTY does not name its root's PID, so the Runner cannot walk its processes; covering it means a change to the recorded Codex patch.docs/runner-isolation.mdsays.