box-firewall takes the no-UFW branch on a UFW host (SIGPIPE under pipefail); surfaced as a test flake #102
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#102
Loading…
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?
test/cli.sh's fresh-UFW block flakes ~2/20 on an unmodified treeFound while running the suite repeatedly during #101's review rounds.
Symptom
The box-firewall fresh-UFW block (
test/cli.sh:1102-1114) intermittentlyfails. The driving check passes:
…but
$WFW/fresh.logis not written, so the four downstream greps that readit (
:1105,:1107,:1111,:1114) all fail together. The signature isone passing check followed by exactly four failures in the same block.
Measured
git stashed to baremain), so it is not caused by any in-flight PR.that the sample was too small to see it there.
Reported as measured by the run that found it; I have not independently
re-reproduced the 2/20, and the rate is low enough that a confirming sweep
should be part of diagnosing this rather than a precondition for filing.
Why it matters more than a normal flake
A test that fails ~10% of the time on
maintrains everyone to re-runrather than read. This one is worse than average because its failure mode is
four assertions going red at once from a missing artifact — which looks
alarming and specific, and is neither. That is exactly the shape that gets
waved through as "the flaky firewall thing" right up until the day it is
real.
It also undercuts the sweep-duty rule of re-running cancelled or red checks
after a rebase: the rule assumes a red check means something.
Where to look
The
runfwhelper is attest/cli.sh:1050. The log is produced by theshimmed
ufwwriting to$FAKE_UFW_LOG. Candidates, roughly in order:flush/sync barrier between the
runfwsubshell exiting and the greps.$WFWbeing reused or cleaned between the write and the reads.runfwstill reports 0, sothe log is never opened and only the greps notice.
Diagnosis should start by making the failure visible — on a missing
fresh.log, dump what is in$WFWand what the shim's stderr was, so thenext occurrence reports its own cause instead of four content-free greps.
Not a blocker for anything in flight
Pre-existing, unrelated to #99/#101. Filed so it is tracked rather than
re-discovered.
Correction: this is a production bug, not a test flake
Diagnosed in #106. The title and framing of this issue understate it — recording that here so the record is not misleading.
Two corrections to the report above:
runfw ufwblock flakes, not just fresh — remap, boot and fresh all hit it across a 40-run baseline (5 failures, 12.5%, so the reported ~2/20 holds).fresh.logis written. It contains exactlyufw statusand nothing else. "Not written" was a fair inference from four blank grep failures, but absent-vs-present-but-empty was the whole diagnosis.The defect is in
host/box-firewall.sh:22, not in the test:Status: activeis the first line ufw prints.grep -qmatches and exits immediately, closing the pipe while ufw is still writing the table; ufw takes SIGPIPE.grepreturns 0 — but this file runs underset -euo pipefail, so the pipeline returns 141. Theifreads false, and a host with UFW plainly active takes the no-UFW nft branch, never building the DNS carve-out its persisted rules depend on.None of the three candidate causes guessed above was it.
Evidence (isolated outside the suite):
PIPESTATUS = "141 0"; the trigger is the reader's early exit specifically —grep -qmatching on the first line failed 14/2000,grep -c(which drains) 0/2000,grep -qmatching on the last line 0/2000; and an A/B of the real script under shims went 7/1500 wrong-branch → 0/1500 fixed.Real
ufwis a Python program with a slower, longer write than the test shim's singleprintf. Production is not safer — it just has no assertion watching it.Related, deliberately not swept into #106:
drill/wipe.sh:120,drill/doctor.sh:287andhost/teardown-host.sh:60carry the identicalufw status | grep -qshape but do not setpipefail, so the SIGPIPE is discarded and the branch holds. Latent rather than live — a futureset -o pipefailin any of them turns it real.Correction to my correction:
host/teardown-host.shwas a LIVE instance, not a latent oneThe comment above claimed all three sibling
ufw status | grep -qsites were safe because none setpipefail. @claude-bot-andresmgsl and @codex-bot-andresmgsl both caught that independently in #106, and they are right.host/teardown-host.sh:12isset -euo pipefail— onmaintoo, not introduced by anything in flight. Line 60 was the identical pipeline:So the same race could read a plainly-active UFW as inactive and silently skip the entire UFW crumb-removal block, leaving stale
boxnet/claudenetrules behind on a host the operator was told is clean. It is a branch condition, soerrexitnever fires — no error, no red X. And this script runs unattended (CI's uninstall drill,box uninstall --purge-host), which is the same "flaky and nobody is watching" class the original fix warned about.The numbered-delete loop at line 62 had the same defect for a different reason:
while sudo ufw status numbered | grep -q "on $net"is also an early-exit reader, so it could end while rules remained.Both are fixed in #106 — captures instead of pipes, matching
box-firewall.sh's pattern — and the CHANGELOG claim is corrected rather than deleted.Still accurate from the earlier comment:
drill/wipe.shanddrill/doctor.shset onlyset -u, so the SIGPIPE is discarded and their branches hold. Latent, not live, until either gainspipefail. (drill/doctor.shalready captures intoufw_outanyway.)