perf(macos): pass seatbelt profile to sandbox-exec via temp file (-f)#303
Open
shawnm-anthropic wants to merge 3 commits into
Open
perf(macos): pass seatbelt profile to sandbox-exec via temp file (-f)#303shawnm-anthropic wants to merge 3 commits into
shawnm-anthropic wants to merge 3 commits into
Conversation
Each sandboxed command interpolated the often ~100KB seatbelt profile inline into the wrapped command string (sandbox-exec -p "<profile>"). The outer shell had to re-parse the quoted profile and the kernel copied it through argv twice — measured at ~45-60ms of overhead per sandboxed command on macOS. The profile is now written once per command to a 0600 file under a per-user temp directory and passed via sandbox-exec -f <file>, keeping the wrapped command string ~1KB. Stale profile files older than 5 minutes are swept on each wrap call (covers crashed processes), and the wrap falls back to inline -p if the file cannot be written. Because the profile now lives in a file rather than the unsandboxed wrapper's argv, a sandboxed command that could write to the profile directory would control the policy applied to the NEXT sandboxed command — a sandbox escape. Every generated profile therefore denies file-write* under the profile directory (plus the standard move-blocking rules), even when the config imposes no other write restrictions, and writeMacOSProfileFile() refuses directories that are not exclusively owned by the current user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…apply) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the repo's existing srt- temp naming (srt-ca-, SANDBOX_RUNTIME=srt). Co-Authored-By: Claude Opus 4.8 <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.
What
Each sandboxed command on macOS interpolated the seatbelt profile (tens to hundreds of KB depending on config) inline into the wrapped command string (
sandbox-exec -p "<profile>"). The outer shell has to re-parse the quoted profile and the kernel copies it through argv twice.Measured on this branch with a 50KB profile (hyperfine, 60 runs,
echo hiwrapped bywrapCommandWithSandboxMacOS): 54.2ms ± 2.6ms inline-pvs 34.5ms ± 5.6ms via-f— 1.57× faster, ~20ms saved per sandboxed command. The gap grows with profile size; Claude Code (which carries this change as a patch on its vendored copy, with a ~110KB profile) measured ~45-50ms saved per sandboxed Bash command end-to-end.The profile is now written once per command to a 0600 file under a per-user temp directory (
$TMPDIR/sbx-profiles-<uid>, 0700) and passed viasandbox-exec -f <file>, keeping the wrapped command string ~1KB. Stale profile files older than 5 minutes are swept on each wrap call (covers crashed processes), and the wrap falls back to inline-pif the file cannot be written.Security: the profile file must be tamper-proof from inside the sandbox
With inline
-p, the profile traveled in the argv of the unsandboxed wrapper — nothing inside the sandbox could touch it. With-f, a sandboxed command that can write to the profile directory could overwrite the profile file of the next sandboxed command (e.g. with(version 1)(allow default)) beforesandbox-execreads it — a sandbox escape, same class as the existing protections for git hooks and dangerous dotfiles. Two layers close this:file-write*under the profile directory, plus the standard move-blocking rules (file-write-unlink/file-write-createon the subpath and literal denies on ancestors, so the directory can't be renamed away and recreated, or replaced with a symlink). This applies even when the config imposes no other write restrictions (e.g. network-only sandboxes), and regardless of what the caller puts inallowWrite.writeMacOSProfileFile()refuses a directory that is not exclusively ours (wrong owner, not a directory, or group/other-writable) and falls back to inline-p— covers a pre-createdsbx-profiles-<uid>in a shared world-writable /tmp.The directory path is realpath-resolved (
/var/folders→/private/var/folders) so the deny rule matches the canonical path seatbelt evaluates.Tests
New
test/sandbox/macos-profile-file.test.ts:-f, profile file is 0600 inside the profile dir, command string stays small-pwhen the profile dir is not exclusively ownedsandbox-exec(macOS): wrapped command executes; a sandboxed command cannot create or overwrite files in the profile dir even when its parent is write-allowed; the same write succeeds outside the profile dir (sanity)Existing tests that asserted profile content against the wrapped command string now read it via a shared
getProfileFromWrappedCommand()helper (handles both-fand-p).Full suite on macOS: 342 pass / 5 fail — the 5 failures are
--control-fdtests that fail identically on cleanmainon this machine (pre-existing, unrelated).🤖 Generated with Claude Code