Skip to content

Close a real sandbox escape: dir_fd= bypasses the workspace write grant - #53

Merged
Shashankss1205 merged 1 commit into
mainfrom
fix/sandbox-dirfd-escape
Aug 2, 2026
Merged

Close a real sandbox escape: dir_fd= bypasses the workspace write grant#53
Shashankss1205 merged 1 commit into
mainfrom
fix/sandbox-dirfd-escape

Conversation

@Shashankss1205

Copy link
Copy Markdown
Collaborator

check_path() resolves a call's path argument against the process's cwd, pinned to the workspace by os.chdir(). Every mutating os function that accepts dir_fd=/src_dir_fd=/dst_dir_fd= resolves the same string against an open directory descriptor instead, and cwd never enters into it — so a tool that legitimately opens a runtime path for reading (site-packages, which has to stay readable so imports work) can get a descriptor for it and then write there via that descriptor while check_path validates a path that has nothing to do with where the write actually lands.

Verified end to end: a tool opens site-packages (a legitimate read), gets a dir_fd from it, and calls os.open("evil.pth", O_CREAT|O_WRONLY, dir_fd=that_fd). No SandboxViolation was raised, the file landed in site-packages, and it executed automatically on the next fresh interpreter start in that environment — the exact .pth-drop escape the read/write grant split (and the existing os.readlink guard) exists to close, reopened through a syscall shape the audit-event path check never considered a path argument for at all. os.mkdir, os.remove, and os.rename with dir_fd= reproduce the same hole.

Fix: _install_dir_fd_guard() wraps every stdlib function that accepts one of the three dir_fd keyword spellings (chmod, chown, link, mkdir, mkfifo, mknod, open, remove, rename, replace, rmdir, symlink, unlink, utime) and refuses the keyword outright, the same way the existing os.readlink guard already refuses dir_fd for the one function it covers — there is no reliable way to turn a directory descriptor back into the path it names, so there is nothing to check against the grant. Derived from the stdlib's own three keyword spellings rather than a hand-maintained per-function table, so it does not go stale the way the fork_exec arity table already did once.

Added test_gate_dot_pth_cannot_be_planted_via_dir_fd (4 parametrized cases: os.open, os.mkdir, os.remove, os.rename), sitting next to the existing plain-path .pth-planting gate test it mirrors.

Verified: full suite green on Python 3.12 and 3.13; ruff clean; confirmed normal (non-dir_fd) file operations inside the workspace are unaffected.

check_path() resolves a call's path argument against the process's cwd,
pinned to the workspace by os.chdir(). Every mutating os function that
accepts dir_fd=/src_dir_fd=/dst_dir_fd= resolves the same string against an
open directory descriptor instead, and cwd never enters into it — so a tool
that legitimately opens a runtime path for reading (site-packages, which has
to stay readable so imports work) can get a descriptor for it and then write
there via that descriptor while check_path validates a path that has nothing
to do with where the write actually lands.

Verified end to end: a tool opens site-packages (a legitimate read), gets a
dir_fd from it, and calls os.open("evil.pth", O_CREAT|O_WRONLY,
dir_fd=that_fd). No SandboxViolation was raised, the file landed in
site-packages, and it executed automatically on the next fresh interpreter
start in that environment — the exact .pth-drop escape the read/write grant
split (and the existing os.readlink guard) exists to close, reopened through
a syscall shape the audit-event path check never considered a path argument
for at all. os.mkdir, os.remove, and os.rename with dir_fd= reproduce the
same hole.

Fix: _install_dir_fd_guard() wraps every stdlib function that accepts one of
the three dir_fd keyword spellings (chmod, chown, link, mkdir, mkfifo,
mknod, open, remove, rename, replace, rmdir, symlink, unlink, utime) and
refuses the keyword outright, the same way the existing os.readlink guard
already refuses dir_fd for the one function it covers — there is no reliable
way to turn a directory descriptor back into the path it names, so there is
nothing to check against the grant. Derived from the stdlib's own three
keyword spellings rather than a hand-maintained per-function table, so it
does not go stale the way the fork_exec arity table already did once.

Added test_gate_dot_pth_cannot_be_planted_via_dir_fd (4 parametrized cases:
os.open, os.mkdir, os.remove, os.rename), sitting next to the existing
plain-path .pth-planting gate test it mirrors.

Verified: full suite green on Python 3.12 and 3.13; ruff clean; confirmed
normal (non-dir_fd) file operations inside the workspace are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Shashankss1205
Shashankss1205 merged commit 729c6c5 into main Aug 2, 2026
6 checks passed
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.

2 participants