Skip to content

fix(macos): pin PATH on the share sweep's guest round trip - #162

Merged
h1d3mun3 merged 2 commits into
mainfrom
fix/macos-sweep-path-pin
Aug 1, 2026
Merged

fix(macos): pin PATH on the share sweep's guest round trip#162
h1d3mun3 merged 2 commits into
mainfrom
fix/macos-sweep-path-pin

Conversation

@h1d3mun3

@h1d3mun3 h1d3mun3 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

The problem

The share sweep's guest round trip invoked a bare python3:

ssh_macos "$vm" "python3 -c '$(_macos_msync_program)' …" < "$list"

augur's own ~/.augur-env puts $HOME/.local/bin first in the PATH of every guest shell —
including the non-login shell ssh <host> <cmd> runs, which still sources ~/.zshenv. So a guest
that writes ~/.local/bin/python3 decides what that call returns.

verify_macos_egress_locked has pinned its own probes against exactly this since #131
(augur:1748-1755, with the reasoning written out). The sweep did not, and the asymmetry was
unexplained.

Why this call site and not the tripwire's

This one is the mechanism, not the check. A shadowed interpreter here prints ok=N while
invalidating nothing, so the operator is told their edit reached the guest when it did not — and the
edit that did not land may be the one tightening the rules on a guest that is misbehaving. Shadowing
only verify_macos_share_freshness would make the self-test lie while real sweeps still worked.

It also runs far more often, and unattended: four attach points plus every
AUGUR_MACOS_REFRESH_INTERVAL seconds for the life of the VM, where the tripwire runs twice per up.

The tripwire's own three round trips are still unpinned. That is the remaining half of the finding and
is deliberately not in this PR — it is the check rather than the mechanism, and it is worth its own
argument about the removal signal it carries.

Scope, stated so it is not read as more than it is

Not host code execution: the guest's response is case-matched and echoed, never evaled or
sourced. Not an egress widening: no invariant in INVARIANTS.md covers guest-returned text. What this
protects is mechanism integrity — that a sweep reporting success actually invalidated something.

PATH pinning does not stop a shell function or an alias. python3() { … } in ~/.zshenv — the
file augur itself appends to — defeats it, measured. This removes the cheapest PATH-resolved vector
and brings the sweep in line with its sibling; it does not make the guest's answer trustworthy. The
sibling's pin has the same hole, and the new comment says so at the point of use.

One correction carried into the comment

The rationale at verify_macos_egress_locked says the real tools are safe because a project clone
"has no sudo grant to write" system directories. That is falseaugur:3202-3204 uses password
sudo against project VMs. The actual reason is the sealed, read-only system volume with SIP on,
measured on a 26.6 guest: / is apfs, sealed, local, read-only, csrutil status: enabled, and
ls -lO /usr/bin/python3 shows restricted. The new comment records the correction rather than
repeating the wrong premise.

Verification

One executable line changes; the rest of the augur hunk is comment.

tests/41 gains two arms: the pin precedes python3 on the recorded command line (order is the
property — a pin appended after the interpreter has resolved is not a pin), and the NUL-separated list
still crosses on stdin untouched, so the pin did not cost the wire protocol.

Mutation-checked: removing the pin turns the first arm red against a 126/0 control.

Source: docs/security-reviews/2026-07-28-egress.md, §6 item 39(a).

A second commit, and why it is in this PR

tests/41's ssh_macos stub drained the suite's inherited stdin on every unpiped call:

ssh_macos() {                 # only ever driven with the piped `python3 -c` shape here
    printf '%s\n' "$*" >> "$SSHLOG"
    cat > "$STDINLOG"

The header was true when written. It stopped being true when the share-refresh mode arms began
driving cmd_claude_macos and cmd_shell_macos, which reach the stub as -t interactive shapes
with nothing piped. Under CI stdin is /dev/null so cat sees EOF and it is invisible; on a
developer's terminal it is the TTY and make offline-tests stops dead with no error and no timeout —
verbatim the failure tests/39_no_stdin_block.sh exists for.

It was live on main. Measured on a pristine origin/main worktree: tests/41 completes in 26 s
with an ordinary stdin and does not finish in 70 s with a FIFO held open. tests/39 did not catch it
because its discovery rule is "any offline test that mentions .augur-env", and tests/41 did not —
until the comment in this PR's other commit mentioned it. So the coverage extension is accidental and
the bug it found is not.

Fixed the way tests/36 and tests/38 already do it, and ordered first so both commits are green
on their own: without the arms, tests/41 is not yet a tests/39 candidate, and with the stub already
fixed the arms cannot turn it red.

Verification

ALL GREEN (30 scripts) on the branch head. At the intermediate commit, tests/39 5/0 and tests/41
124/0. tests/41 at the head: 126/0, and it completes in 26 s with stdin held open.

🤖 Generated with Claude Code

h1d3mun3 and others added 2 commits August 1, 2026 21:59
… calls

    ssh_macos() {                 # only ever driven with the piped `python3 -c` shape here
        printf '%s\n' "$*" >> "$SSHLOG"
        cat > "$STDINLOG"

The header was true when it was written and stopped being true when the
share-refresh mode arms began driving `cmd_claude_macos` and `cmd_shell_macos`.
Those reach the stub as `-t` interactive shapes with nothing piped, and the
unconditional `cat` then drains whatever stdin the SUITE inherited.

Under CI stdin is /dev/null, so `cat` sees EOF and the bug is invisible. On a
developer's terminal it is the TTY: `make offline-tests` stops dead, no error, no
timeout. That is verbatim the failure tests/39_no_stdin_block.sh exists for — see
its header for the two files that shipped in this state before this one.

Measured on a pristine `origin/main` worktree: tests/41 completes in 26 s with an
ordinary stdin and does not finish in 70 s with a FIFO held open. tests/39 did not
catch it because its discovery rule is "any offline test that mentions
`.augur-env`", and tests/41 did not.

Fixed the way tests/36 and tests/38 already do it — discriminate on the command
shape rather than draining unconditionally. `printf %s` is excluded AHEAD of the
`python3 -c` arm because the freshness tripwire's probes carry both: their pipe is
inside the remote command string, so from the stub's side they are unpiped like the
interactive shapes, and matching `python3 -c` alone would regrow this the first time
this file drives them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h report

The sweep's guest round trip invoked a bare `python3`, while augur's own
`~/.augur-env` puts `$HOME/.local/bin` FIRST in the PATH of every guest shell —
including the non-login shell `ssh <host> <cmd>` runs, which still sources
`~/.zshenv`. A guest that plants `~/.local/bin/python3` therefore decides what that
round trip returns.

verify_macos_egress_locked has pinned its own probes against exactly this since
#131 (augur:1748-1755, with the reasoning written out). The sweep did not, and the
asymmetry was unexplained.

THIS SITE, NOT THE TRIPWIRE'S. It is the mechanism, not the check. A shadowed
interpreter here prints `ok=N` while invalidating nothing, so the operator is told
their edit reached the guest when it did not — and the edit that did not land may be
the one tightening the rules on a guest that is misbehaving. Shadowing only
verify_macos_share_freshness would make the self-test lie while real sweeps still
worked. It also runs at four attach points AND every refresh interval for the life
of the VM, unattended, where the tripwire runs twice per `up`. The tripwire's own
three round trips stay unpinned here: that is the check rather than the mechanism,
and it carries the ADR-0016 §5 removal signal, which deserves its own argument.

NOT A COMPLETE ANSWER, said at the point of use so it is not read as one: PATH
pinning does not stop a shell FUNCTION or an alias. `python3() { … }` in `~/.zshenv`
— the file augur itself appends to — defeats it, measured. This removes the cheapest
PATH-resolved vector and brings the sweep in line with its sibling; it does not make
the guest's answer trustworthy, and the sibling's pin has the same hole.

One correction rides along in the comment: the sibling says the real tools are safe
because a project clone "has no sudo grant to write" system directories. That is
false — augur:3202-3204 uses password sudo against project VMs. The reason is the
sealed read-only system volume with SIP on, measured on a 26.6 guest: `/` is `apfs,
sealed, local, read-only`, `csrutil status: enabled`, `ls -lO /usr/bin/python3` shows
`restricted`.

One executable line changes; the rest of the augur hunk is comment. tests/41 gains
two arms — the pin precedes `python3` on the recorded command line (order is the
property: a pin appended after the interpreter has resolved is not a pin), and the
NUL list still crosses on stdin untouched. Mutation-checked: removing the pin turns
the first arm red against a 126/0 control.

Source: docs/security-reviews/2026-07-28-egress.md §6 item 39(a).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🧭 Egress drift reminder

This PR changes the egress core (augur / augur-proxy/ / gvproxy/ / augur.conf / augur-vm/) but does not touch docs/security-reviews/.

If this change alters the egress posture, please:

  • run /egress-survey (the bounded drift audit), and
  • if it drifted: add a new dated snapshot under docs/security-reviews/ and/or update docs/security-reviews/INVARIANTS.md.

Non-blocking reminder. It does not verify correctness (that is the egress fail-closed E2E + swift test). If no doc update is needed, ignore this.

@h1d3mun3
h1d3mun3 merged commit a89bc12 into main Aug 1, 2026
3 checks passed
@h1d3mun3
h1d3mun3 deleted the fix/macos-sweep-path-pin branch August 1, 2026 13:31
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.

1 participant