fix: the racing-reader sweep guards the class, and revoke-user captures the trust store #127
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#127
Loading…
Reference in a new issue
No description provided.
Delete branch "test/racing-reader-sweep-class"
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?
Class versus instance, one level up
#120 was careful to pin the class rather than the site: it replaced the
host/teardown-host.sh-only check with a sweep over everyhost/*.shanddrill/*.sh, so a new script inherits the pin instead of being one more thingto remember. That was the right instinct, and it stopped one level short of
its own conclusion. The matcher it swept with was:
grepis an instance spelling. The hazard is notgrep— it is anyreader that can stop before the writer is done. These are the same defect
with different letters:
Every one closes the pipe early, every one SIGPIPEs
ufwmid-table, every oneyields 141 under
pipefail, and every one produces exactly the wrong answer#102 was about. A sweep that catches four spellings of a five-spelling class is
a sweep that will be quietly wrong the first time someone reaches for
head.So: both halves of the matcher are alternations now.
The reader half — why it is not narrowed to early-exit spellings
The obvious refinement is to match only the readers that actually exit early:
grep -qbut notgrep -c,sed -n '1p;q'but notsed 's/x/y/',awk '… exit'but not a full-drainawk. Deliberately not done, for tworeasons that point the same way:
That precision rots. It is a regex trying to decide whether an arbitrary
awkprogram contains a reachableexit. It will be subtly wrong, and itwill be wrong in the direction of silence — which is the failure mode this
entire issue chain exists to eliminate.
The strict rule costs nothing here. Every
ufw statussite in the treealready captures first — checked, all six:
Capture-then-match is already the house idiom. Banning the pipe outright
forbids nothing anyone writes, and it cannot be defeated by a spelling
nobody thought to enumerate. I checked every hit before widening: the
alternation over-matches nothing in the tree — the sweep is green on an
unmodified branch, and the only way to make it fire is to plant the shape.
The
revoke-user.shquestion — verdict: in scope, and it is the stronger halfThe issue asks whether the sweep should cover non-
ufwwriters, and flagshost/revoke-user.sh:206as the nearest analog. Yes — extended. The issueframes this as "realistically un-racy today, so defensive". That is true about
the race, and I want to be precise that it undersells the guarantee, which
is what changed my answer from "scope to (1)" to "do both":
host/revoke-user.sh:13isset -euo pipefail.pipefailis alreadyon. This is not #107's situation.
drill/wipe.shwas protected by a missingsetline and was "one line from wrong"; this line has no such protection atall. The only thing standing between it and a wrong answer is the trust
store happening to fit in one write.
set -e-exempt too. It sits left of&&, so a 141 does not evenabort — same structural reason #102 was invisible.
the
--purgeleftover assert, the block whose own comment reads "Assertabsence rather than trusting exit codes". A 141 reads as "no leftover cert"
on a host that still trusts the revoked user's certificate — and
revoke --purgethen prints success instead ofpurge INCOMPLETE. The operator istold access is gone while the credential that grants it is still in the trust
store.
"Un-racy because the writer is small" is a claim about incus's current output
size, made by a script that cannot check it. That is a fine reason not to call
this a live defect — I am not claiming a bug anyone has hit — and a poor
reason to leave a security-adjacent assert depending on it. Cost of fixing it
is one capture.
The leading newline preserves the
^anchor thegrephad. Verifiedequivalent to the original across nine anchoring cases rather than asserted —
first line, middle line, last line, empty capture, and the four near-misses
that must not match (
notincus-user-1000,,xincus-user-1000,,incus-user-10000,against uid 1000, and a bareincus-user-1000with nocomma). All nine agree with the old
grep, in both directions.Writers are enumerated, not generalised — and that is the honest limit
I did not extend the sweep to "any multi-line writer feeding a reader",
even though that is the real class. That matcher is unwritable here. A survey
of
host/*.sh drill/*.sh bin/boxturns up ~150| grep/| head/| awksites, and nearly all of them are
printf '%s\n' "$var" | grep -q …— readingan already-captured string back out. Those are the fix pattern, not the bug
pattern, and no regex separates them from the real thing.
So the sweep claims exactly what it can check: these named writers are
never piped into a reader. It grows one writer at a time, and each addition is
a deliberate act with a reason attached. An enumerated list that is true beats
a universal claim that is unenforceable — the latter is how a pin becomes
decoration.
Left alone, deliberately
id -nG | tr ' ' '\n' | grep -qxinhost/grant-user.sh(:34,:50,:80),host/revoke-user.sh(:44,:49),host/setup-host.sh(
:149,:154),drill/. The issue names these as explicitly out of scopeand I agree:
id -nGis one short line, single write, no realistic window.Untouched.
host/revoke-user.sh:186—done < <(incus config trust list …). Readsthe same writer, but through process substitution feeding a
whileloopthat drains to EOF. Not a pipeline, so
pipefaildoes not observe it, andthe loop has no early
break. Safe by construction; left as is. Worthnaming because the sweep does not match it and someone will wonder.
drill/doctor.sh:208— already captured, confirmed again here.Proof the new arms bite
A sweep widened without demonstrating the new arms fire is not widened. Each
spelling was planted one at a time in a swept file (
drill/wipe.sh),suite run, then reverted:
drill/wipe.shsudo ufw status | head -n1racing reads in: drill/wipe.sh· 478 passed, 1 failedsudo ufw status | sed -n '1p;q'racing reads in: drill/wipe.sh· 478 passed, 1 failedsudo ufw status | awk '/Status/ {print; exit}'racing reads in: drill/wipe.sh· 478 passed, 1 failedsudo ufw status | read -r firstracing reads in: drill/wipe.sh· 478 passed, 1 failedincus config trust list --format csv | grep -q "^incus-user-1,"racing reads in: drill/wipe.sh· 478 passed, 1 failedsudo ufw status | grep -q "Status: active"(#120's original arm, regression check)racing reads in: drill/wipe.sh· 478 passed, 1 failedFailure text in each case:
And the real fix, verified the same way — reverting
host/revoke-user.shtoits pre-PR state makes the sweep name the actual file, not a plant:
Restored: 479 passed, 0 failed.
Those last two are the other-direction pins, added for the same reason
#120 added them for
teardown-host.shandwipe.sh: without them the sweepcan be satisfied by deleting the leftover assert instead of fixing it, which
would be a strictly worse outcome than the bug. The negative sweep says "no
pipe"; the positive pins say "and the capture is still there doing its job".
Checks
bash test/cli.sh— 479 passed, 0 failed (477 on #120's base, +2 newrevoke-userpins)bash test/labels-reconcile.sh— 19 passed, 0 failedbash test/release.sh— 90 passed, 0 failedshellcheck -xover CI's exact list (shopt -s globstar; files=(bin/* **/*.sh))— clean, 15 files
.github/scripts/changelog-armed.sh—VERSION '0.8.1-dev' agrees with the top section (Unreleased)On the changelog, given #122
#122 is open about a PR that deleted a release heading while editing
CHANGELOG.md. This entry was inserted, never written over a line, andthat was verified rather than trusted:
Zero deleted lines of any kind, heading count unchanged. The
## 0.8.0 — 2026-07-19heading is untouched.Closes #124
Verdict: I agree with everything and have no additional feedback.
The capture-first changes remove the early-reader race for both writers, the widened sweep covers the intended class, and the CLI suite passes (479/0).
Verdict: Approve — I agree with this as-is.
Tip
3ffc1dc(stacked on #120) finishes the racing-reader class on both axes the instance pin left open.What holds
grep|head|sed|awk|readall close early and produce the same 141-under-pipefail wrong answer. Not narrowing to early-exit flags is the right trade: regex precision rots toward silence; capture-first is already the house idiom.incus config trust listjoinsufw statusfor a real fail-open site:revoke-userleftover assert underset -euo pipefail, left of&&(141 is set -e-exempt). A generic multi-line-writer ban would false-positive ~150 legitimate re-reads of captured strings.[[ ]]with leading newline preserves^-style first-row anchoring without the pipe. Defensive today; correct under any futurepipefailtweak.✅ Reviewed — I agree with all of this, no concerns.
First review. The reader alternation is the right widening —
head,sed -n '1p;q',awk '… exit'andreadall produce the identical 141 — and not narrowing to early-exit spellings is the correct trade: a regex that tries to decide whether an awk program has a reachableexitrots toward silence, and capture-first is already the house idiom at all sixufw statussites. Therevoke-user.shfix is the stronger half for exactly the reason stated:set -euo pipefailis already on there, the site sits left of&&so a 141 isset -e-exempt, and it fails open on the one assert whose job is to prove closure.Verified independently: the sweep regex cannot false-positive on the fix pattern itself — in
numbered="$(sudo ufw status numbered 2>/dev/null || true)"the[^|]*can only reach the first|of||, after which(grep|head|sed|awk|read)fails to matchtrue, so capture lines pass. And the[[ $'\n'"$trust_csv" == *$'\n'"incus-user-$uid,"* ]]match is equivalent to the old^-anchored grep given the prepended newline, with$uidnumeric so no glob metacharacters can enter the pattern. The other-direction pins (capture present, not just pipe absent) close the delete-the-assert loophole, which is the failure mode that would otherwise satisfy the sweep.— automated review by
claude-bot-andresmgsl· heavy-duty-review-bot✅ Approved — I agree with all of this, no concerns.
Formalizing my earlier ✅ review of this same revision as an approval — reviews from this bot now carry a formal verdict state instead of a comment.
— automated review by
claude-bot-andresmgsl· heavy-duty-review-bot#120 has landed (
4fe91ef), so this is unblocked and rebased ontomain.It came down to the single commit this PR predicted it would (
256ded2). #120's commit was carried here only because the branch was cut from it; with its content now onmain, the replay was a no-op and it dropped out of the rebase. Thetest/cli.shoverlap the description warned about resolved itself for the same reason — this branch only ever edited lines #120 added, and those lines are nowmain's.The diff is exactly this PR's own work and nothing else:
Both changelog entries coexist correctly — #120's
drill/wipe.shentry (now arriving viamain) and this one's sweep entry sit side by side under### Fixed, no heading rewritten.changelog-monotonic.shconfirms all 4 headings at the new merge base survive.Green locally, mirroring CI:
shellcheck -xoverbin/* **/*.sh,test/cli.sh489/489,test/labels-reconcile.sh72/72,test/release.sh134/134, changelog armed + monotonic.Re-requesting the panel — the force-push staled the round you approved. Nothing in this PR's own content changed since then; the rebase only removed a commit.
Verdict: Approve — I agree with this as-is.
Tip
256ded2(rebased onto main after #120 landed) is the same racing-reader class work previously approved at3ffc1dc, now a single commit with #120's stack commit gone.What holds
grep|head|sed|awk|readall close early and produce the same 141-under-pipefail wrong answer. Not narrowing to early-exit flags is the right trade: regex precision rots toward silence; capture-first is already the house idiom at everyufw statussite.incus config trust listjoinsufw statusfor a real fail-open site:revoke-userleftover assert underset -euo pipefail, left of&&(141 is set -e-exempt). A generic multi-line-writer ban would false-positive ~150 legitimate re-reads of captured strings.[[ ]]with leading newline preserves^-style first-row anchoring without the pipe.CI green (check / reconcile / rehearsal). No blockers.
✅ Approved — I agree with all of this, no concerns.
Re-approving after the rebase onto
main: head moved from3ffc1dcto256ded2when #120 landed and its stacked commit dropped out, exactly as the PR predicted. I compared the rebased diff against the revision I approved — the PR's own content (widened sweep alternation, thetrust_csvcapture inhost/revoke-user.sh, the two other-direction pins, the inserted changelog entry) is unchanged; the only difference is that #120's commit now arrives viamain, and thetest/cli.shoverlap resolved as described since this branch only edited lines #120 added. CI is green on256ded2(check, reconcile, rehearsal). Nothing new to raise.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botVerdict: I agree with everything and have no additional feedback.
Re-reviewed the renewed request at head
256ded2after the rebase. The current diff preserves the previously approved capture-first fix, widened writer/reader sweep, positive pins, and append-only changelog entry; all current check runs are successful.Round summary — handing off to @danmt
All three bots approved on head
256ded2, every verdict after the head commit:grok-bot-andresmgslclaude-bot-andresmgslcodex-bot-andresmgslNo changes requested. The only change since the previous round is the rebase onto merged #120, described above: #120's commit dropped out of the replay because its content is now
main's, leaving the single commit this PR always said it would reduce to. This PR's own content is untouched.Green locally, mirroring CI:
shellcheck -xoverbin/* **/*.sh,test/cli.sh489/489,test/labels-reconcile.sh72/72,test/release.sh134/134, changelog armed + monotonic.Not setting
merge-next— #129 holds it and is also ready for you. The two are independent (this toucheshost/revoke-user.sh, #129 touchesbin/box) and meet only inCHANGELOG.md, so merge them in either order: whichever goes second takes a positional changelog rebase and I will handle it without a re-review round.I also cleared the
blockedlabel, which was pointing at #120 and is agent-owned, so the sweep would not have removed it.