Skip to content

fix(macos): one transient empty read discarded the whole freshness verdict - #164

Merged
h1d3mun3 merged 1 commit into
mainfrom
fix/share-freshness-tripwire-flake
Aug 3, 2026
Merged

fix(macos): one transient empty read discarded the whole freshness verdict#164
h1d3mun3 merged 1 commit into
mainfrom
fix/share-freshness-tripwire-flake

Conversation

@h1d3mun3

@h1d3mun3 h1d3mun3 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes the make e2e failure seen while validating #163 — which turned out to be
unrelated to that PR and to be a real defect in verify_macos_share_freshness.

  FAIL the freshness self-test ran
         ↳ no verdict in the up output at all

What was measured

One Apple Silicon host, one VM, three runs — three different outcomes:

how it was run before after verdict
probes as separate ssh connections stale (correct) correct Shared-file refresh verified
tight 12-iteration loop already current, 12/12 correct no longer need → E2E skips
the real up --macos reconcile, twice EMPTY Could not verify … → E2E fails

The guest was demonstrably alive in the third case: a manual cat of that same
probe from inside it returned the correct current value moments later.

So a release-gated self-test was non-deterministic, and one transient empty read
discarded the whole verdict. A check whose answer depends on scheduling teaches
the operator to ignore it.

The fix, and its limit

The two value-bearing reads (warm, after) now survive a transient empty
answer — 3 attempts, 0.2 s apart, both numbers borrowed rather than invented
(_MACOS_SWEEP_TRIES and the guest-poll backoff already used here). Worst case
this adds 0.8 s to up --macos.

Important

Only on empty, and that limit is the whole point. A NON-EMPTY but WRONG
after is the SELF-TEST FAILED verdict — the finding this tripwire exists
to produce. Retrying it would hand a broken guest three chances to eventually
agree, converting "the mitigation no longer works on this guest OS" into a clean
pass: a check that cannot fail, which is the #131/#136/#137 shape exactly. The
loop breaks on the first non-empty answer whatever it says.

before is deliberately not retried — its expected value is the stale one, so
extra attempts would bias the run toward no longer need for a reason that is
this function's own latency rather than the platform's behaviour.

The cause of the empty read is not established, and nothing here claims it is.
Every reproduction that read the probe successfully differed from up in ways
never narrowed down — most obviously that up runs a full-share sweep immediately
before this function, which none of them did. This defends against the
observation, not against a theory.

Two messages that sent an investigator the wrong way

  • the empty-after warn said "the guest stopped answering mid-check". It was
    answering. Naming a cause that was never measured cost a real debugging session
    chasing gvproxy. It now states what was observed. The warm-read warn likewise
    stops reporting an empty answer and a wrong answer as the same observation.
  • tests/e2e_macos_vm.sh's *) arm reported "no verdict in the up output at
    all"
    for four outcomes, three of which do print Could not verify the shared-file refresh: …. It now surfaces that line and claims silence only when
    there is none. Still a fail — this is a release gate.

PATH pin

The tripwire's two python3 probes are now pinned like the sweep's. #162 pinned
the mechanism and its own commit message said "The sibling's pin has the same
hole"
— this is the sibling. The disclaimer carries over unsoftened: it stops a
PATH-resolved python3, not a shell function or alias.

Tests

tests/42 gained a section for the retry and its limit (12 arms). The one
that matters is the wrong-but-non-empty arm: its mode list is one longer than the
run needs, with the CORRECT value parked where only a retry could reach it — so if
a future change ever retries a non-empty answer, the arm flips to "verified" and
fails loudly.

Mutation-tested. Removing the break from the retry loop makes 7 arms fail,
including a wrong but non-empty answer is reported as BROKEN. The arms have
teeth; they are not decoration.

Two fixture defects surfaced while writing them and are fixed here:

  • survives rcfail rcfail rcfail rcfail named a mode that was never a case
    arm
    . It fell through to the fresh fallback, so an assertion about
    non-zero-return protection was made against a stub returning zero.
  • a mode list that ran out fell back to fresh, letting an under-counted arm test
    a healthy guest while its name claimed otherwise. Exhausting the list is now
    recorded and checked once, as the last assertion in the file.

Verified / not verified

Verified here: make offline-tests ALL GREEN (30 scripts), make shellcheck
clean (including the strict pass, which covers tests/e2e_macos_vm.sh),
bash -n augur OK, and the mutation test above.

Not verified here: the live macOS E2E on Apple Silicon. This host is itself a
VZ guest, so nested virtualisation is unavailable. AUGUR_TEST_LIVE=1 make e2e
still needs a human — that is the run that will confirm the up --macos reconcile
path now reaches a verdict.

One thing this does not fix

A subagent reported that of four full-suite runs on the unmodified base, one
came back 1/30 script(s) FAILED and it did not reproduce in three further runs.
The script was not identified. I could not reproduce it either. Flagging it
because "make offline-tests must end ALL GREEN" is currently not a fully
trustworthy gate, and a rare red here would be easy to misattribute to this
retry. Worth its own issue.

🤖 Generated with Claude Code

…rdict

`verify_macos_share_freshness` is a release-gated self-test, and it was
non-deterministic. Measured on an Apple Silicon host, one machine, one VM,
three runs:

- issued as separate ssh connections it reproduced staleness and its
  `after` read was correct -- the "verified" verdict
- run as a tight twelve-iteration loop the `before` read came back ALREADY
  CURRENT every single time -- the "no longer need" verdict, 12 for 12
- on the real `up --macos` reconcile path, twice, the `after` read came
  back EMPTY -- and the guest was demonstrably alive, since a manual `cat`
  of that same probe from inside it returned the correct current value
  moments later

The third outcome is what `make e2e` hit. A check whose answer depends on
scheduling teaches the operator to ignore it, so this makes the two
value-bearing reads survive a transient empty answer.

ONLY on empty, and that limit is the point. A NON-EMPTY but WRONG `after`
IS the SELF-TEST FAILED verdict -- the finding this tripwire exists to
produce. Retrying that would hand a broken guest three chances to
eventually agree and convert "the mitigation no longer works on this guest
OS" into a clean pass: a check that cannot fail, which is the #131/#136/#137
shape exactly. The new loop breaks on the first non-empty answer whatever
it says. Three attempts and 0.2 s are borrowed, not invented -- three
matches `_MACOS_SWEEP_TRIES`, 0.2 s matches the guest-poll backoff already
used here. Worst case this adds 0.8 s to `up --macos`. The `before` read is
deliberately NOT retried: its expected value is the stale one, so extra
attempts would bias the run toward "no longer need" for a reason that is
this function's own latency.

THE CAUSE OF THE EMPTY READ IS NOT ESTABLISHED and nothing here claims it
is. Every reproduction that read the probe successfully differed from `up`
in ways never narrowed down -- most obviously that `up` runs a full-share
sweep immediately before this function, which none of them did. This
defends against the observation, not against a theory.

Also, two messages that sent an investigator the wrong way:

- the empty-`after` warn said "the guest stopped answering mid-check". The
  guest was answering. Naming a cause that was never measured cost a real
  debugging session chasing gvproxy. It now states what was observed, and
  the warm-read warn likewise stops reporting an empty answer and a wrong
  answer as the same observation.
- `tests/e2e_macos_vm.sh`'s `*)` arm reported "no verdict in the up output
  at all" for four outcomes, three of which DO print "Could not verify the
  shared-file refresh: ...". It now surfaces that line and claims silence
  only when there is none. Still a `fail` -- this is a release gate.

And the tripwire's own two `python3` probes are now PATH-pinned like the
sweep's. #162 pinned the mechanism and its commit message said "The
sibling's pin has the same hole" -- this is the sibling. The disclaimer
carries over unsoftened: it stops a PATH-resolved `python3`, not a shell
function or alias.

tests/42 grew a section for the retry and its limit. The arm that matters
is the wrong-but-non-empty one: its mode list is one longer than the run
needs, with the CORRECT value parked where only a retry could reach it, so
if a future change ever retries a non-empty answer that arm flips to
"verified" and fails loudly. Two fixture defects surfaced while writing it
and are fixed here: `survives rcfail rcfail rcfail rcfail` named a mode
that was never a case arm, so it fell through and asserted non-zero-return
protection against a stub returning ZERO; and a mode list that ran out used
to fall back to `fresh`, letting an under-counted arm test a healthy guest
while claiming otherwise. Exhausting the list is now recorded and checked
once at the end.

Verified: `make offline-tests` ALL GREEN (30 scripts), `make shellcheck`
clean, `bash -n augur` OK. Not verified here: the live macOS E2E, which
needs an Apple Silicon host.

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 commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Live E2E on Apple Silicon: 28 passed · 0 failed · 1 skipped

AUGUR_TEST_LIVE=1 make e2e is fully green on a real Apple Silicon host, after
bash install (so the binary and script under test are this branch's).

# VM-mode egress fail-closed
  ok   allowlisted domain reachable in the VM (api.github.com → HTTP 200)
  ok   non-allowlisted domain blocked in the VM (example.com severed by the proxy)
  ok   IP-literal direct egress severed in the VM (no raw-routing bypass)

# Shared-file refresh — a host edit reaches a RUNNING guest
  ok   a host edit became visible in the running guest within 5s, with no augur command
  ok   the freshness self-test reproduced staleness and then cleared it

This closes the one item both PRs listed as "not verified here".

About the earlier red run

An intermediate run failed a host edit became visible in the running guest
(still MARK-A after 30s). That was not a defect in either PR. A diagnostic
harness I had handed the maintainer earlier called start_share_refresher from an
AUGUR_SOURCE_ONLY=1 source ./augur context, where MACOS_SHARE is unbound —
it is assigned at augur:6679, 54 lines after the seam returns at augur:6625.
Under set -u that kills macos_share_roots, but only inside the
< <(macos_share_roots …) process substitution, so the refresher loop survives and
enumerates zero files every tick: alive, with a live pidfile, sweeping nothing. It
ran that way for ~31 hours (1.1 MB of MACOS_SHARE: unbound variable in the
refresher log, mtime confirming it was still going during the E2E), and
start_share_refresher's idempotence (share_refresher_running && return 0) meant
the E2E's own up left it in place.

After augur down --macos cleared it, the arm passes in 5s — one refresh interval.

That underlying seam ordering is a real latent defect and gets its own PR: the
project's own AUGUR_SOURCE_ONLY test seam cannot reach the sweep path at all,
which is why nothing offline covers it.

@h1d3mun3
h1d3mun3 marked this pull request as ready for review August 3, 2026 11:02
@h1d3mun3
h1d3mun3 merged commit 12e14ee into main Aug 3, 2026
3 checks passed
@h1d3mun3
h1d3mun3 deleted the fix/share-freshness-tripwire-flake branch August 3, 2026 11:06
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