fix(sandbox): symlink-safe host file transfer (P0-2)#102
Merged
Conversation
…utputs (M1) I1 (security): stage_inputs now lstat-checks that `inputs_dir` is a real directory after mkdir — a kernel-planted `session/inputs -> host_dir` symlink is rejected before any file is written (O_NOFOLLOW only covers the final path component, not the parent). M1 (contract): _collect_outputs wraps both _outputs_dir() and iterdir() in try/except OSError so execute() is never disrupted by an unreadable outputs dir. Cleanups: remove unused `from pathlib import Path` in test_file_utils_nofollow; make FIFO test hang-proof with a thread watchdog; reorganize imports in test_sandbox_transfer (move mid-file imports to top, drop unused `import os`). Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
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.
Summary
Fixes P0-2 (Critical) from the 2026-07-10 comprehensive review: the stateful (Docker) sandbox's host-side file copies followed attacker (in-container kernel) symlinks — enabling arbitrary host writes (input staging) and host-secret exfiltration (output collection). 4 commits; offline suite
1923 passed.What's fixed
copy_regular_file_no_follow(file_utils.py) — a reusable no-follow copy primitive: source openedO_RDONLY | O_NOFOLLOW | O_NONBLOCK+S_ISREGcheck (rejects symlinks/FIFOs/devices; a FIFO fails fast instead of hanging); dest written to a private temp + atomicos.replace(a pre-planted destination symlink is replaced, not written through).tools/sandbox/manager.py::stage_inputs) — rejects symlink/special-file sources, never follows a destination symlink, and rejects a symlinkedinputsdirectory (a kernel-plantable parent that would otherwise redirect the write to the host).tools/sandbox/backends/jupyter_docker.py) — extracted_collect_outputs(offline-testable); skips symlink/special entries and a symlinkedoutputsdir (no host-secret exfil), best-effort / never-raise; the shared outputs dir is now0o700(was0o777).Threat model
The in-container kernel runs untrusted, model-generated code and owns the
/workspacebind mount, so it can plant symlinks ininputs/andoutputs/(including the directories themselves). Every host-side copy now refuses to follow them — source or destination, final component or the kernel-plantable parent.Verification
1923 passed, 1 skipped (gated LaTeX), 26 xfailed. New tests are host-side (real symlinks +os.mkfifo), no container needed; the-m dockerlive suite is unaffected.O_NOFOLLOW/os.replaceguard only the final path component, so a symlinkedinputs/outputsdirectory needed a symmetriclstatguard — reproduced as an arbitrary host write, then fixed with a regression test.Not in scope (separate Tier-3 cycles)
sandbox_data_mountsbind-mount sources (P0-1 / P1-4).https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv