sandbox: chdir into tmpdir before exec to avoid getcwd EPERM#22440
Merged
Conversation
`Sandbox#deny_read_home` (dd8119e) deny-reads home subpaths such as `~/Documents`, `~/.ssh`, `~/.aws` inside the build/postinstall sandbox. The sandboxed child inherits the user's CWD from the parent `brew` invocation, so when the user runs `brew install` from a CWD under one of those subpaths, the first `getcwd(3)` in the child (e.g. in `Resource#unpack` or `Mktemp#run`'s block-form `Dir.chdir`) walks across a denied parent and the install crashes with: Error: An exception occurred within a child process: Errno::EPERM: Operation not permitted - getcwd Move the sandbox child into its own tmpdir (under `HOMEBREW_TEMP`) before `exec` so every subsequent `getcwd` walks only `/opt/homebrew` parents. The non-block `Dir.chdir` does not itself call `getcwd` and so works even from a denied CWD. The deny rules added in dd8119e are unchanged. Only source-style installs were affected; bottles take a different path that already `cd`s into the Cellar before `getcwd` runs.
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.
brew lgtm(style, typechecking and tests) with your changes locally?Diagnosis and patch were produced with Claude Code. The author read the upstream source (
sandbox.rb,utils/fork.rb,mktemp.rb,resource.rb), the macOS kernel log (log show … Sandbox), and the failingHOMEBREW_DEBUG=1stack traces directly before, during, and after the change — I'm vouching for the fix on the merits, not because an LLM produced it. Earlier sessions (including one in jamessawle/osch#67) chased two wrong hypotheses (eagerPathname.pwdinResource#unpack, and macOS TCC tightening) before kernel-log + source reading landed on this one; that history is in the issue for anyone curious about what was not the bug.Summary
Since commit dd8119e ("Harden sandboxed install phases", 2026-05-27),
Sandbox#deny_read_homedeny-reads sensitive home subpaths (~/Documents,~/.ssh,~/.aws,~/.gnupg,~/.config/gcloud,~/Library/Keychains,~/Dropbox, etc.) inside the build and postinstall sandboxes.The sandboxed child inherits the user's CWD from the parent
brewinvocation. When the user runsbrew install <source-style-formula>from a CWD under one of those denied subpaths, the firstgetcwd(3)in the child —getcwdwalks every parent inode callingfile-read-metadata— hits the deny rule, returnsEPERM, and the install crashes with:The first
getcwdhappens to live inResource#unpack(eagercurrent_working_directory = Pathname.pwdat the top of the method), but removing that just defers the EPERM by one frame toMktemp#run's block-formDir.chdir(Ruby's block-formDir.chdircallsgetcwdinternally to save the previous directory). There are likely more such call sites downstream. So this PR fixes the underlying cause rather than the first symptom: it moves the sandboxed child into a non-denied directory beforeexec, so every subsequentgetcwdwalks only/opt/homebrew/...parents.The patch
In
Sandbox#run's child branch, just beforeexec-ing into the sandbox profile,Dir.chdir(tmpdir)(wheretmpdiris the per-invocationDir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP)already allocated a few lines up). The non-block form ofDir.chdirdoes not itself callgetcwd, so this works even when starting from a denied CWD — onlychdir(2)runs.What this does NOT change
cdintoHOMEBREW_CELLAR/<name>/<version>before anygetcwdruns); behaviour is unchanged there.Resource#unpack's eagerPathname.pwdis left alone — with the CWD now underHOMEBREW_TEMPit walks only safe parents, and removing it would be a real (if usually-dead) semantic change for relative-target callers per a01715f.Repro
Before this patch, on macOS with Homebrew including dd8119e:
Kernel log during the failure:
Stack trace (with
HOMEBREW_DEBUG=1), showing both candidate first-getcwdsites depending on which one you fix:Verification
Tested locally on macOS 26.5 arm64,
Homebrew 5.1.14-41-g0cba9a2:cd ~/Documents/foo && brew reinstall jamessawle/tap/osch(source-style)cd ~ && brew reinstall jamessawle/tap/osch(source-style)cd ~/Documents/foo && brew reinstall jq(bottle)brew tests --only=sandboxbrew tests --only=resourcebrew lgtm(typecheck + style + changed tests)Tests
Added
sandbox_spec.rbregression: stand up a freshSandbox,deny_read_patha tmpdir,Dir.chdirinto it, and run/bin/pwdthrough the sandbox. Without the patch this raisesErrorDuringExecution(sandbox-exec exits 1 fromgetcwdEPERM); with the patch it succeeds. Verified both directions locally by stashing/unstashing thesandbox.rbchange.Links