Uh oh!
There was an error while loading. Please reload this page.
fix(cosign-retry): stop stale-artifact fail-open and equals-form fail-closed (cli#568) - #570
Conversation
…-closed (cli#568) scripts/cosign-retry.sh tracked --output-* paths in a way that failed both directions (two Bugbot findings on PR #563): - FAIL OPEN on stale artifacts: retry attempts never cleared prior --output-* files, so a leftover non-empty .cert/.sig from an EARLIER failed attempt satisfied the `[ -s ]` fail-closed check after a later `exit 0` that wrote nothing -- reporting a signature that does not exist, the one property the harness exists to prevent. Fix: remove every parsed --output-* path before each attempt, so `[ -s ]` is a claim about the current attempt alone. - FAIL CLOSED on equals-form flags: outputs_from_args set prev unconditionally, so after --output-x=val the token still matched --output-* and the next arg was misparsed as a phantom output path, failing a genuine equals-form sign with exit 3. Fix: reset prev after handling the equals form. Tests (scripts/tests/cosign-retry-verify.sh): add a stale-leftover case (exits 0 before the fix, now fails closed), equals-form parse cases, and a wipe-ordering guard. The two bug-catching cases were confirmed to FAIL against the pre-fix script; all 19 pass on the fix. shellcheck --severity=error and bash -n clean, matching the build.yml gates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saadqbal
left a comment
There was a problem hiding this comment.
Approving. Two defects in opposite directions from one root cause — a model of "which files should exist" derived once and never reconciled with reality — and both fixes are placed where the property actually lives rather than patched at the symptom.
The wipe is inside the retry loop, before the attempt. That is the whole fix: it makes [ -s "$out" ] a claim about this attempt instead of about the filesystem. Clearing once before the loop would have left attempts 2..N reading attempt 1's debris, which is the original bug with a smaller window.
On the equals-form, the continue is load-bearing and not decoration.prev='' alone would be undone by prev="$a" at the loop bottom, so the flag would still match --output-* on the next iteration and swallow the following token. Skipping that assignment is what actually closes it. Worth stating because a future tidy-up that "simplifies" the continue away reopens the bug silently.
The evidence is the right kind. Confirming case 15 and 16 fail against the pre-fix script with the exact reversed codes — rc=0 where 3 was wanted, rc=3 where 0 was wanted — is a demonstration rather than a claim, and it is the one thing that distinguishes a real regression test from a test that happens to pass.
Case 18 is the one I would have asked for: the wipe could plausibly have broken the success path, so proving a transient-then-clean-resign still exits 0 guards the fix's own risk rather than only the bug it fixes.
Updating the header invariant in the same change keeps the file's stated contract true — the docstring now says the files are removed before each attempt, which is the sentence someone will rely on next.
26 checks green, no open threads.
Uh oh!
There was an error while loading. Please reload this page.
Closes#568
What
Two Bugbot findings on
scripts/cosign-retry.sh(introduced by #563), both about how the retry harness tracks--output-*paths. The script's model of "which files should exist after this attempt" was derived once and never reconciled with reality, so it failed both directions.Defect 1 — fail OPEN on stale artifacts (Medium)
Retry attempts never cleared prior
--output-*files. The fail-closed check only tests[ -s "$out" ], so a leftover non-empty.cert/.sigfrom an earlier failed attempt was treated as proof that a laterexit 0(which wrote nothing) had signed — the harness reporting a signature that does not exist.Fix: remove every parsed
--output-*path immediately before each cosign attempt, so[ -s ]is a claim about the current attempt alone.Defect 2 — fail CLOSED on equals-form flags (Low)
outputs_from_args()setprev="$a"unconditionally, so after an equals-form flag (--output-x=val)prevstill matched--output-*and the following token was misparsed as a phantom output path — failing a genuine equals-formsign-blobwith exit 3 on a path cosign was never asked to write.Fix: reset
prevafter handling the equals form; both space-separated and=valueforms keep working.Tests
Extended
scripts/tests/cosign-retry-verify.sh(the harnessbuild.ymlruns), keeping its style of driving the real script through a PATH-shimmed fakecosign:.cert/.sigthen dies transient; attempt 2 exits 0 writing nothing → must fail closed (exit 3).--output-x=pathflags must succeed (exit 0), not fail closed on phantom paths.Evidence
rc=0want 3 — reported "signed on attempt 2/3" off attempt-1 debris; case 16:rc=3want 0 — good sign failed closed on--output-signature=…), and all 19 pass on the fix.shellcheck --shell=bash --severity=errorandbash -nclean on both files (thebuild.ymlgates).Header property #3 (FAIL CLOSED) updated to describe the per-attempt clearing so the doc matches the code.
🤖 Generated with Claude Code
Note
Medium Risk
Touches fail-closed signing verification used in
release.yml; the change reduces false “signed” outcomes rather than weakening gates, but incorrect path parsing or wipe ordering could still break legitimate retries.Overview
Fixes two bugs in how
scripts/cosign-retry.shtracks--output-*paths during release signing retries.Stale artifacts (fail-open): The script now deletes every parsed
--output-*path before each cosign attempt, so the post-success[ -s ]check only reflects what the current attempt wrote. Without that wipe, leftover.cert/.sigfrom a failed retry could make a laterexit 0with no new output look like a successful sign.Equals-form flags (fail-closed):
outputs_from_argsnow clears its “previous token” state after--output-x=path, so the next CLI argument is not treated as a phantom output file. Space-separated--output-x pathbehavior is unchanged.The verify harness adds four cases (stale leftover, equals-form success, equals-form empty output, wipe-before-retry happy path) plus a
run_under_test_equalshelper. Header comments for property #3 are updated to match the per-attempt wipe.Reviewed by Cursor Bugbot for commit b8ec411. Bugbot is set up for automated code reviews on this repo. Configure here.