fix(tests): a pipefail pipeline must not end in a reader that exits early #622
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/guard-sigpipe"
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?
tests/binjovi.shexited 141 and failed a seanfarm build on 2026-09-10 with every assertion in it passing. 141 is 128+13: SIGPIPE.This is a wrong answer, not a crash
grep -qexits on the first match. The producer upstream is then killed by SIGPIPE, andpipefailreports the pipeline as 141 — for a match. Anifreads 141 as false, so a guard concludes "not found" about a thing it just found.head -Ndoes the same after N lines.Measured here:
The producer has to still be writing when the reader leaves, so a small input usually wins. A guard passes for years and inverts the day its input grows. That is what made this look like a flake instead of a hole.
The repository already met this once
tests/cnpg-restore-drill-trap.shcarries the finding in its own words:That fixed the one scan in front of it. The pattern was live at 102 more sites across 23 guards.
The substitutions
Exact, and not
|| true, which would swallow a genuine miss:cmd | grep -q PATcmd | grep PAT >/dev/nullcmd | head -1cmd | awk 'NR==1'cmd | head -Ncmd | awk 'NR<=N'grepwithout-qstill exits 0 on a match and 1 on a miss. It reads to the end first, so there is nobody left to kill.The guard does not just grep for the pattern
tests/no-sigpipe-readers.shscans every guard that setspipefailand refuses a pipeline ending ingrep -*qorhead.Six controls prove the detector fires on
grep -q,grep -Fqandhead -1, stays quiet on both safe replacements, and does not read comments.Three more re-measure the premise at run time. The guard fails if
grep -qon a large producer stops returning 141, if the replacement stops reporting a match, or if it stops reporting a genuine miss — so the rule cannot rot into one nobody can justify.It exempts itself by name, with a comment, because it is the one file that has to spell the pattern out.
How it was done
A mechanical transformer, then
bash -non every touched file, then the full suite twice.The first transformer appended the redirect at end of line and produced
missing+=("issuer" >/dev/null).bash -ncaught it. That pass was thrown away rather than patched around, and rewritten to find where thegrepcommand actually ends with a quote- and paren-aware scan. Two sites it could not place confidently were done by hand.99 guards pass.
https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8
tests/binjovi.sh exited 141 and failed a seanfarm build on 2026-09-10 with every assertion in it passing. 141 is 128+13: SIGPIPE. THIS IS A WRONG ANSWER, NOT A CRASH, and that is why it matters. `grep -q` exits on the first match. The producer upstream is then killed by SIGPIPE, and `pipefail` reports the pipeline as 141 -- for a MATCH. An `if` reads 141 as false, so a guard concludes "not found" about a thing it just found. `head -N` does the same after N lines. Measured here: { echo MATCH; seq 1 100000; } | grep -q MATCH exit 141 { echo MATCH; seq 1 1; } | grep -q MATCH exit 0 { echo MATCH; seq 1 100000; } | grep MATCH >/dev/null exit 0 seq 1 100000 | grep MATCH >/dev/null exit 1 The producer has to still be writing when the reader leaves, so a small input usually wins the race. A guard passes for years and inverts the day its input grows. That is what made this look like a flake. THE REPOSITORY ALREADY MET THIS ONCE. cnpg-restore-drill-trap.sh carries the finding in its own words -- "the guard reported 'no archive destination' for a manifest that opens with one... Measured on this pattern: 40 runs out of 40 returned 141, none returned 0" -- and fixed the one scan in front of it. The pattern was live at 102 more sites across 23 guards. The substitutions are exact, and NOT `|| true`, which would swallow a genuine miss: cmd | grep -q PAT -> cmd | grep PAT >/dev/null cmd | head -1 -> cmd | awk 'NR==1' cmd | head -N -> cmd | awk 'NR<=N' `grep` without -q still exits 0 on a match and 1 on a miss. It reads to the end first, so there is nobody left to kill. tests/no-sigpipe-readers.sh holds the property. It scans every guard that sets pipefail and refuses a pipeline ending in `grep -*q` or `head`. Six controls prove the detector fires on `grep -q`, `grep -Fq` and `head -1`, stays quiet on both safe replacements, and does not read comments. Three more re-measure the premise itself at run time: the guard fails if `grep -q` on a large producer stops returning 141, if the replacement stops reporting a match, or if it stops reporting a genuine miss. The guard exempts itself BY NAME, because it is the one file that has to spell the pattern out. The first transformer appended the redirect at end of line and produced `missing+=("issuer" >/dev/null)`. `bash -n` caught it. The pass was thrown away rather than patched, and rewritten to find where the grep command actually ends with a quote- and paren-aware scan. Claude-Session: https://claude.ai/code/session_01TdW5FSvRJW8CtGnpd29Xp8