Skip to content

fix(sandbox): symlink-safe host file transfer (P0-2)#102

Merged
shoom1 merged 4 commits into
developfrom
fix/p02-symlink-safe-sandbox-transfer
Jul 12, 2026
Merged

fix(sandbox): symlink-safe host file transfer (P0-2)#102
shoom1 merged 4 commits into
developfrom
fix/p02-symlink-safe-sandbox-transfer

Conversation

@shoom1

@shoom1 shoom1 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

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 opened O_RDONLY | O_NOFOLLOW | O_NONBLOCK + S_ISREG check (rejects symlinks/FIFOs/devices; a FIFO fails fast instead of hanging); dest written to a private temp + atomic os.replace (a pre-planted destination symlink is replaced, not written through).
  • Input staging (tools/sandbox/manager.py::stage_inputs) — rejects symlink/special-file sources, never follows a destination symlink, and rejects a symlinked inputs directory (a kernel-plantable parent that would otherwise redirect the write to the host).
  • Output collection (tools/sandbox/backends/jupyter_docker.py) — extracted _collect_outputs (offline-testable); skips symlink/special entries and a symlinked outputs dir (no host-secret exfil), best-effort / never-raise; the shared outputs dir is now 0o700 (was 0o777).

Threat model

The in-container kernel runs untrusted, model-generated code and owns the /workspace bind mount, so it can plant symlinks in inputs/ and outputs/ (including the directories themselves). Every host-side copy now refuses to follow them — source or destination, final component or the kernel-plantable parent.

Verification

  • Offline suite: 1923 passed, 1 skipped (gated LaTeX), 26 xfailed. New tests are host-side (real symlinks + os.mkfifo), no container needed; the -m docker live suite is unaffected.
  • Built via subagent-driven development (per-task TDD + spec/quality review) and an opus whole-branch review. Its notable catch: O_NOFOLLOW/os.replace guard only the final path component, so a symlinked inputs/outputs directory needed a symmetric lstat guard — reproduced as an arbitrary host write, then fixed with a regression test.

Not in scope (separate Tier-3 cycles)

  • P0-1 (config trust model), P0-5 (webfetch SSRF).
  • Authorization of which host paths may be staged, and special-file rejection for sandbox_data_mounts bind-mount sources (P0-1 / P1-4).

https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv

shoom1 added 4 commits July 11, 2026 19:41
…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
@shoom1 shoom1 merged commit c85fa90 into develop Jul 12, 2026
2 checks passed
@shoom1 shoom1 deleted the fix/p02-symlink-safe-sandbox-transfer branch July 12, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant