fix(sandbox): grant git-admin dirs for linked worktrees (#30)#31
fix(sandbox): grant git-admin dirs for linked worktrees (#30)#31NoRiceToday wants to merge 5 commits into
Conversation
`omac sandbox run` grants the workdir read+write. For a plain clone that
covers `.git` (it lives inside the workdir), but a linked worktree splits
repo state: the workdir is `<repo>/.worktrees/<name>/` while the git-admin
dir is `<repo>/.git/worktrees/<name>/` — a sibling outside the workdir. So
`git add`/`commit` hit EPERM writing the object store, refs, reflogs and
per-worktree index/HEAD, and the agent can't commit from any worktree.
Detect a linked worktree (`.git` is a file), resolve the shared common dir
via the admin dir's `commondir`, and restore plain-clone parity by granting
the commit-relevant subdirs at the workdir's access level:
- allow (rw): objects, refs, logs, packed-refs, worktrees/<name>
- read-only: config, info (blocks core.hooksPath / credential.helper)
- deny: hooks/ (a writable hooks dir would run un-sandboxed
as the host user on the next host-side commit)
No-op for a plain clone or a submodule (whose admin dir has no `commondir`).
Verified below the process boundary: a real-`git worktree` pipeline test
drives ResolveGrants -> SBPL and bwrap argv generation and asserts the
object store is bound read+write, config read-only, and hooks masked.
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
The worktree grant paths are derived from in-workdir file content (`.git`, `<admin>/commondir`) that a prior sandboxed session could have tampered with, and both backends symlink-canonicalize every grant into a kernel rule. A malicious agent with rw workdir could plant a crafted `.git` + admin/`commondir` chain whose resolved common dir it controls, with `objects` a symlink to a sensitive dir (e.g. ~/.ssh). Because SBPL emits write-allows AFTER protected-path denies (last match wins), the symlink-resolved write grant would punch through the ~/.ssh denial — read+write escape on the next `omac sandbox run` in that workdir. Confine the grant: - Enforce git's structural invariant: admin must be <common>/worktrees/<name>, rejecting a commondir aimed at an unrelated location. - Admit only subdirs whose symlink-resolved path physically lives inside the resolved common dir; drop any entry that escapes (pathWithinRoot). Adds regression tests proving a symlinked `objects`/`refs` never widens a grant to the target, and a spoofed commondir yields no worktree grants. Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
End-to-end testing under real Seatbelt showed the earlier "make packed-refs writable" change was both ineffective and unnecessary: - git 2.x's ref transaction tries to create <common>/packed-refs.LOCK in the common-dir root on every ref update. That root is deliberately not granted (it holds config and hooks), so the lock EPERMs — but git falls back to the loose-ref path and the commit succeeds (verified: exit 0, fsck clean, refs correct). Granting the packed-refs *file* rw cannot silence this: the blocker is the lock sibling in the ungranted root. - Granting the common root rw to allow the lock would, on macOS, let a config write slip through (SBPL emits write-allows last, after the protected denies), defeating the core.hooksPath guard — the exact vector TNG#30 closes. So the harmless lock noise is the accepted cost. A normal add/commit only ever writes loose refs under refs/; packed-refs is only rewritten by gc/pack-refs, which need that ungranted lock anyway. So packed-refs is correctly read-only (git still reads it for status/log and for committing onto a packed ref). Documented the limitation on gitWorktreeGrants. Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
|
Note Resolved in Post-merge finding from host e2e testing (linked-worktree commit)Ran a real Finding 1 — repo commit hooks are blocked, breaking hook-enforced workflows
Finding 2 —
|
Flip the linked-worktree shared hooks dir from a hard deny to read-only: readable+runnable so host-authored commit hooks (prepare-commit-msg, commit-msg, pre-commit) execute inside the sandbox, but never writable so the agent still cannot plant a hook that would run un-sandboxed on the host's next commit (the TNG#30 persistence vector). The read grant stays inside the contained() symlink-containment check, since unlike a deny it would otherwise follow a planted hooks->secret symlink to the target. Add a real-Seatbelt e2e (rooted under $HOME, not $TMPDIR, so the baseline temp-write grant doesn't mask the read-only hooks grant) proving the hook runs during a sandboxed commit while the hooks dir stays unwritable. Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Fixes #30.
Problem
omac sandbox rungrants the workdir read+write. For a plain clone that covers.git(it lives inside the workdir), so git works. For a linked worktree,git splits state: the workdir is
<repo>/.worktrees/<name>/but the git-admindir is
<repo>/.git/worktrees/<name>/— a sibling outside the workdir. Sogit add/commithit EPERM writing the object store, refs, reflogs and theper-worktree index/HEAD, and an agent can't commit from any linked worktree.
Fix
Detect a linked worktree (
.gitis a file), resolve the shared common dir viathe admin dir's
commondir, and grant the commit-relevant subdirs at theworkdir's access level:
objects/,refs/,logs/,worktrees/<name>/config,info,packed-refs(blockscore.hooksPath/credential.helpermutation)hooks/(a writable hooks dir would run un-sandboxed as the host user on the next host-side commit)No-op for a plain clone or a submodule (whose admin dir has no
commondir).Security hardening
Grant paths are derived from in-workdir file content (
.git,commondir) aprior sandboxed session could tamper with, and both backends symlink-canonicalize
grants into kernel rules. A planted
objects -> ~/.sshsymlink would otherwiseemit a write-allow on the target and — since SBPL write-allows come after the
protected denies — punch through the
~/.sshdeny (sandbox escape). Confined byenforcing git's
<common>/worktrees/<name>structural invariant and admittingonly entries whose symlink-resolved path physically lives inside the resolved
common dir.
Verification
git worktree,plus regressions for the symlink-escape and spoofed-
commondirvectors.binary commits cleanly (exit 0, fsck clean); pre-hardening binary allowed the
symlink escape, hardened binary denies it (secret untouched).
Known limitation
git branch -d/-Dandgit tag -dfail inside a sandboxed worktree — git'sdeletion transaction needs a lock in the ungranted common-dir root — but fail
cleanly (exit 1, ref preserved, no corruption). Commits emit a non-fatal
packed-refs.lockEPERM yet succeed;gc/pack-refsref-packing no-ops. Allother operations (commit/amend/branch/checkout/rebase/stash) are unaffected.