Sandbox execs a symlinked wrapped tool via its realpath, breaking argv0 self-location (linuxbrew brew prefix) #184
Labels
No labels
burndown-2026-06
sunday-sprint
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/cli-guard#184
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
When the sandbox jails a wrapped tool that is a symlink, it stashes and execs the tool's fully
EvalSymlinks-resolved realpath (cli/sandbox/sandbox_linux.go,installToolShim). A tool that self-locates fromargv0(its own invoked path) then computes the wrong base directory, because the realpath can sit in a structurally different location than the canonical PATH symlink.Concrete failure
Homebrew on the standard linuxbrew layout.
/home/linuxbrew/.linuxbrew/bin/brewis a symlink into/home/linuxbrew/.linuxbrew/Homebrew/bin/brew.bin/brewderivesHOMEBREW_PREFIXfromargv0's grandparent on every run and never reads it from the environment:.../.linuxbrew/bin/brew-> prefix.../.linuxbrew(correct)..../.linuxbrew/Homebrew/bin/brew(what the jail execs) -> prefix.../.linuxbrew/Homebrew, whose Cellar is empty.So inside the jail every brew operation targets a phantom empty prefix:
brew upgradereports installed kegs "not installed",brew installrebuilds from source into an off-PATH prefix. Full trace and the launcher walkthrough are in ward#546 / ward#543 / ward#537.Why the current resolution is too aggressive
installToolShimEvalSymlinksthe canonical path only to obtain a target distinct from the shim-masked entry (so the exec does not loop back into the shim). But full symlink resolution over-collapses: it discards the canonical directory structure that self-locating tools read fromargv0. Resolving just enough to escape the masked inode is not enough here either, since brew's single-level symlink lands exactly on the bad realpath.Suggested fix
For a wrapped tool whose canonical PATH entry is a symlink, re-invoke it from an unmasked alias in the canonical directory (e.g. bind or link a sibling like
<dir>/.<tool>-realalongside the masked<dir>/<tool>), soargv0's parent/grandparent still resolve to the canonical prefix while the exec target still points at the real binary and bypasses the shim. Compiled binaries at their own path (no symlink) keep the current bind-stash behavior. Apple Silicon Homebrew is unaffected either way (prefix == repository, no nesting).Add a regression test that jails a symlinked shim
<root>/bin/tool -> <root>/repo/bin/tooland asserts the exec'dargv0's grandparent is<root>, not<root>/repo.Interim state
ward has a product-layer workaround landed (ward#543):
execBrewRawdrops the jail for the brew exec whenLookPath("brew")and its realpath have different grandparents, running brew via the canonical symlink. That keeps brew working on linuxbrew but leaves brew's build subprocesses un-jailed there. This engine fix would let ward restore the jail for brew (drop that special-case).Director burn-down cleanup: closing this as fixed by commit
8886aee(fix(sandbox): exec a symlinked tool via a canonical-dir symlink, not its realpath), which is onmainand implements the core argv0-preserving symlink behavior requested here. The remaining known drawback from that fix is tracked separately in cli-guard#185: leak-free placement for the.<tool>.cliguardexec symlink.