fix(tests): a pipefail pipeline must not end in a reader that exits early #622

Merged
binjovi-bot merged 1 commit from fix/guard-sigpipe into trunk 2026-09-11 03:51:49 +00:00
Owner

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

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   <- a match, reported as failure
{ echo MATCH; seq 1 1;      } | grep -q MATCH          exit 0     <- small input wins the race
{ echo MATCH; seq 1 100000; } | grep MATCH >/dev/null  exit 0
seq 1 100000 | grep MATCH >/dev/null                   exit 1     <- a genuine miss still reports

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.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. Small manifests usually win the race, which is what made it look like a flake instead of a hole.

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:

before after
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.

The guard does not just grep for the pattern

tests/no-sigpipe-readers.sh 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 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 — 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 -n on 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 -n caught it. That pass was thrown away rather than patched around, and rewritten to find where the grep command 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 `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 <- a match, reported as failure { echo MATCH; seq 1 1; } | grep -q MATCH exit 0 <- small input wins the race { echo MATCH; seq 1 100000; } | grep MATCH >/dev/null exit 0 seq 1 100000 | grep MATCH >/dev/null exit 1 <- a genuine miss still reports ``` 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.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. Small manifests usually win the race, which is what made it look like a flake instead of a hole. 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: | before | after | |---|---| | `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. ## The guard does not just grep for the pattern `tests/no-sigpipe-readers.sh` 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 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 — 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 -n` on 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 -n` caught it. That pass was thrown away rather than patched around, and rewritten to find where the `grep` command 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
fix(tests): a pipefail pipeline must not end in a reader that exits early
All checks were successful
binjovi/ci Binjovi completed the frozen plan
f998319ea3
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
binjovi-bot deleted branch fix/guard-sigpipe 2026-09-11 03:51:50 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sean/seanfarm!622
No description provided.