You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Surfaced by fresh-context verification of #2843. Both defects are in the
row-reading loops, not in the detector, so they are independent of the
attribution work that PR adds.
Refs measured at. Findings 1 and 3 are pre-existing on main
(5958e1faf) — the same while read … done < FILE idiom sits at check-silent-revert.sh:449 (verify_known_incidents) and :233
(ack_reason) there. Finding 2's demonstration needs the per-row
attribution assertion, which exists only on #2843's branch, so that transcript
pair was taken at 39ecf3088 (branch ccp-2843-fix), where the functions sit
at :595 and :264 and parse_attribution_field at :575. The underlying
newline behavior is identical on both refs; only the observable differs.
All repros use throwaway files outside the checkout via SILENT_REVERT_INCIDENTS / SILENT_REVERT_ACK
(check-silent-revert.sh:192-193). The shipped corpus and acknowledgment
files were not touched.
1. There is no row-count floor: zero rows verified is a green run
verify_known_incidents loops the incident file and reports rc at the end.
Nothing anywhere counts how many rows it actually verified, so a file that
yields no rows falls straight through to the success banner:
$ SILENT_REVERT_INCIDENTS=<empty file> bash scripts/check-silent-revert.sh --verify-known-incidents
Canary reproduces every recorded incident at the shipped settings.
EXIT=0
A comments-only file is the same:
$ SILENT_REVERT_INCIDENTS=<file of two '#' lines> bash scripts/check-silent-revert.sh --verify-known-incidents
Canary reproduces every recorded incident at the shipped settings.
EXIT=0
Zero ok lines printed, and the run still asserts in its own words that the
canary "reproduces every recorded incident". The blank line above the banner is
the only visible difference from a real green run — the exit status, the
sentence, and the check row are identical.
This is the canary's self-proof lane, so the failure direction is a false
green in the one place a false green is least recoverable: the mechanism that
exists to certify the detector still works reports success when it verified
nothing at all.
2. A file with no trailing newline silently loses its last row
while read -r … done < FILE discards a final line that is not newline-
terminated: read returns non-zero at EOF even though it populated the
variables, so the loop body never runs for that row. The row is not reported,
not counted, not warned about — it simply does not exist as far as the replay
is concerned.
Demonstrated with a two-row temp incidents file whose row 2 carries
deliberately wrong attribution counts (=1 and =2 against the real 853
and 298). Two files, byte-identical except for the final newline:
$ SILENT_REVERT_INCIDENTS=two-rows-nonl.txt bash scripts/check-silent-revert.sh --verify-known-incidents
ok f603880da fires as recorded, 1 attribution(s) reproduced exactly (row 1, correct)
Canary reproduces every recorded incident at the shipped settings.
EXIT=0
WITH the trailing newline (277 bytes), the same bytes plus \n:
$ SILENT_REVERT_INCIDENTS=two-rows-nl.txt bash scripts/check-silent-revert.sh --verify-known-incidents
ok f603880da fires as recorded, 1 attribution(s) reproduced exactly (row 1, correct)
FAIL cc58cbc53 fires, but NOT as recorded (row 2, deliberately WRONG counts)
recorded attribution:
bfb66beb8c1a6cdb52957a9a2614c6d2593739dd 1
eda5ae5ed5c2b54fdf2db5fd3b620a9272c46da5 2
what the detector reported:
bfb66beb8c1a6cdb52957a9a2614c6d2593739dd 853
eda5ae5ed5c2b54fdf2db5fd3b620a9272c46da5 298
A row that fires for the wrong reason is not a reproduction.
Do NOT edit the row to match; find out why the attribution moved.
The canary no longer reproduces the incidents it was built for.
Do not relax the recorded expectations to make this pass.
EXIT=1
One byte is the difference between a caught regression and a green run. This
compounds with finding 1: dropping the only row of a one-row file lands
exactly on the zero-rows-verified green above.
3. ack_reason drops the acknowledgment file's last row too — fail-closed
ack_reason (:264 on the branch, :233 on main) uses the same idiom
against $ACK_FILE. Same two-file demonstration, 131 vs 130 bytes, with the
acked commit 6f0a31109 as row 2:
$ SILENT_REVERT_ACK=ack-nl.txt bash scripts/check-silent-revert.sh --commit 6f0a3110942375fb292112f5df5e2c39bbf56cb0
acknowledged 6f0a31109 fix(repo-fleet-hygiene): restore GraphQL merge evidence and rollups after #2633 (#2640)
reviewed and cleared: row 2, the ack under test
EXIT=0
$ SILENT_REVERT_ACK=ack-nonl.txt bash scripts/check-silent-revert.sh --commit 6f0a3110942375fb292112f5df5e2c39bbf56cb0
SILENT REVERT SUSPECTED
removed by 6f0a31109 fix(repo-fleet-hygiene): restore GraphQL merge evidence and rollups after #2633 (#2640)
content from cc58cbc53 fix(repo-fleet-hygiene): report bare repos with live working trees (#2633)
lines lost 447 (threshold 200, window 40 commits)
[... finding body elided ...]
EXIT=1
Lower severity, deliberately stated: this direction is fail-closed. The
lost row means an acknowledged commit fires anyway — noise a human dismisses,
not a missed detection. It is filed with the other two because it is the same
one-line defect in the same file, and fixing one loop while leaving its twin is
how the second one gets forgotten.
What already mitigates this — and where the mitigations stop
t_shipped_data_files_are_wellformed in scripts/check-silent-revert.test.sh
enforces n >= 2 pinned rows, and on fix(scripts): read revert subjects and assert the canary's replay attributions #2843's branch additionally requires
every shipped fires row to carry a well-formed attribution field. That
closes findings 1 and 2 for the shipped corpus only — it reads $SELF_DIR/silent-revert-incidents.txt by path, never whatever SILENT_REVERT_INCIDENTS names.
CI's editorconfig lane enforces insert_final_newline = true for [*]
(.editorconfig:18; the only unset override is the lockfile section at :63-67), which keeps the shipped files newline-terminated in the tree.
The nuance that matters: that lane lives in .github/workflows/ci.yml
(:92-97), and silent-revert-canary.yml is deliberately not in ci.yml and not in its ci-status aggregate — the workflow's own header
says so and explains why ("Adding it there would make a detection heuristic
able to block merges"). So the newline guard is a property of a different
workflow that the canary neither runs nor depends on. The canary's own lane
enforces nothing about final newlines.
Neither mitigation reaches a custom path.SILENT_REVERT_INCIDENTS and SILENT_REVERT_ACK are unbounded by both: no row floor, no newline
enforcement, no grammar check. Every transcript above is a run at the shipped
settings with only that variable changed.
Suggested fix
Both are one-liners, and the row floor is the one that matters:
Count verified rows in verify_known_incidents and die (or fail) when the
count is zero — an incident file that pins nothing is a broken input, not a
pass. Consider asserting >= 1 fires row specifically, since a clean-only
file also proves nothing about detection.
Use while read -r … || [[ -n "$var" ]] (or equivalent) in both loops so a
final unterminated row is processed rather than dropped.
Worth a test in check-silent-revert.test.sh for each: a zero-row incident
file must not report green, and a two-row file without a trailing newline
must reach the same verdict as the same file with one.
Open threads
Two questions raised while measuring this that are not resolved here and
should not be folded into a fix silently.
a. Can GitHub's squash path produce a multi-line first paragraph?
declares_removal reads the subject as git log -1 --format=%B | head -n 1
(:216-219), while every report line prints --format=%s (:401, :501).
Those two differ in exactly one case: %s joins all lines of the first
paragraph with spaces, whereas %B | head -1 takes only the first physical
line. A two-line first paragraph is therefore the only way the detector's view
of the subject can diverge from the one the report shows a human.
Measured across all first-parent commits of origin/main at 5958e1faf —
comparing %s against the first line of %B in a single git log pass:
TOTAL_FIRST_PARENT=1541
DIVERGENCES=0
(The in-file comment at :216 cites 1527 for a related count; the history has
grown since.) So it is unreachable in this repo's history today. But the
squash message is hand-editable in GitHub's merge dialog, so nothing structural
prevents it — the mechanism is not disproved, only unobserved. Failure
direction is toward firing (the detector would fail to see a declaration
the report displays), which is safe, which is why this is a thread rather than
a finding.
b. Does a .gitattributesdiff=<driver> reach the same code path as diff.external?
The script pins --no-ext-diff and check-silent-revert.test.sh:398-434
proves that pin is load-bearing against diff.external specifically. Nothing
in the script or its tests mentions .gitattributes, diff=<driver>, or textconv, and a scan of #2843's comments turned up no answer either. Whether
a per-path diff driver declared in .gitattributes can reach the same
false-green the diff.external test demonstrates is recorded here as an open
question — not asserted in either direction.
Surfaced by fresh-context verification of #2843. Both defects are in the
row-reading loops, not in the detector, so they are independent of the
attribution work that PR adds.
Refs measured at. Findings 1 and 3 are pre-existing on
main(
5958e1faf) — the samewhile read … done < FILEidiom sits atcheck-silent-revert.sh:449(verify_known_incidents) and:233(
ack_reason) there. Finding 2's demonstration needs the per-rowattribution assertion, which exists only on #2843's branch, so that transcript
pair was taken at
39ecf3088(branchccp-2843-fix), where the functions sitat
:595and:264andparse_attribution_fieldat:575. The underlyingnewline behavior is identical on both refs; only the observable differs.
All repros use throwaway files outside the checkout via
SILENT_REVERT_INCIDENTS/SILENT_REVERT_ACK(
check-silent-revert.sh:192-193). The shipped corpus and acknowledgmentfiles were not touched.
1. There is no row-count floor: zero rows verified is a green run
verify_known_incidentsloops the incident file and reportsrcat the end.Nothing anywhere counts how many rows it actually verified, so a file that
yields no rows falls straight through to the success banner:
A comments-only file is the same:
Zero
oklines printed, and the run still asserts in its own words that thecanary "reproduces every recorded incident". The blank line above the banner is
the only visible difference from a real green run — the exit status, the
sentence, and the check row are identical.
This is the canary's self-proof lane, so the failure direction is a false
green in the one place a false green is least recoverable: the mechanism that
exists to certify the detector still works reports success when it verified
nothing at all.
2. A file with no trailing newline silently loses its last row
while read -r … done < FILEdiscards a final line that is not newline-terminated:
readreturns non-zero at EOF even though it populated thevariables, so the loop body never runs for that row. The row is not reported,
not counted, not warned about — it simply does not exist as far as the replay
is concerned.
Demonstrated with a two-row temp incidents file whose row 2 carries
deliberately wrong attribution counts (
=1and=2against the real853and
298). Two files, byte-identical except for the final newline:WITHOUT the trailing newline (276 bytes):
WITH the trailing newline (277 bytes), the same bytes plus
\n:One byte is the difference between a caught regression and a green run. This
compounds with finding 1: dropping the only row of a one-row file lands
exactly on the zero-rows-verified green above.
3.
ack_reasondrops the acknowledgment file's last row too — fail-closedack_reason(:264on the branch,:233onmain) uses the same idiomagainst
$ACK_FILE. Same two-file demonstration, 131 vs 130 bytes, with theacked commit
6f0a31109as row 2:Lower severity, deliberately stated: this direction is fail-closed. The
lost row means an acknowledged commit fires anyway — noise a human dismisses,
not a missed detection. It is filed with the other two because it is the same
one-line defect in the same file, and fixing one loop while leaving its twin is
how the second one gets forgotten.
What already mitigates this — and where the mitigations stop
t_shipped_data_files_are_wellformedinscripts/check-silent-revert.test.shenforces
n >= 2pinned rows, and on fix(scripts): read revert subjects and assert the canary's replay attributions #2843's branch additionally requiresevery shipped
firesrow to carry a well-formed attribution field. Thatcloses findings 1 and 2 for the shipped corpus only — it reads
$SELF_DIR/silent-revert-incidents.txtby path, never whateverSILENT_REVERT_INCIDENTSnames.CI's editorconfig lane enforces
insert_final_newline = truefor[*](
.editorconfig:18; the onlyunsetoverride is the lockfile section at:63-67), which keeps the shipped files newline-terminated in the tree.The nuance that matters: that lane lives in
.github/workflows/ci.yml(
:92-97), andsilent-revert-canary.ymlis deliberately not inci.ymland not in itsci-statusaggregate — the workflow's own headersays so and explains why ("Adding it there would make a detection heuristic
able to block merges"). So the newline guard is a property of a different
workflow that the canary neither runs nor depends on. The canary's own lane
enforces nothing about final newlines.
Neither mitigation reaches a custom path.
SILENT_REVERT_INCIDENTSandSILENT_REVERT_ACKare unbounded by both: no row floor, no newlineenforcement, no grammar check. Every transcript above is a run at the shipped
settings with only that variable changed.
Suggested fix
Both are one-liners, and the row floor is the one that matters:
verify_known_incidentsanddie(or fail) when thecount is zero — an incident file that pins nothing is a broken input, not a
pass. Consider asserting
>= 1 firesrow specifically, since aclean-onlyfile also proves nothing about detection.
while read -r … || [[ -n "$var" ]](or equivalent) in both loops so afinal unterminated row is processed rather than dropped.
check-silent-revert.test.shfor each: a zero-row incidentfile must not report green, and a two-row file without a trailing newline
must reach the same verdict as the same file with one.
Open threads
Two questions raised while measuring this that are not resolved here and
should not be folded into a fix silently.
a. Can GitHub's squash path produce a multi-line first paragraph?
declares_removalreads the subject asgit log -1 --format=%B | head -n 1(
:216-219), while every report line prints--format=%s(:401,:501).Those two differ in exactly one case:
%sjoins all lines of the firstparagraph with spaces, whereas
%B | head -1takes only the first physicalline. A two-line first paragraph is therefore the only way the detector's view
of the subject can diverge from the one the report shows a human.
Measured across all first-parent commits of
origin/mainat5958e1faf—comparing
%sagainst the first line of%Bin a singlegit logpass:(The in-file comment at
:216cites 1527 for a related count; the history hasgrown since.) So it is unreachable in this repo's history today. But the
squash message is hand-editable in GitHub's merge dialog, so nothing structural
prevents it — the mechanism is not disproved, only unobserved. Failure
direction is toward firing (the detector would fail to see a declaration
the report displays), which is safe, which is why this is a thread rather than
a finding.
b. Does a
.gitattributesdiff=<driver>reach the same code path asdiff.external?The script pins
--no-ext-diffandcheck-silent-revert.test.sh:398-434proves that pin is load-bearing against
diff.externalspecifically. Nothingin the script or its tests mentions
.gitattributes,diff=<driver>, ortextconv, and a scan of #2843's comments turned up no answer either. Whethera per-path diff driver declared in
.gitattributescan reach the samefalse-green the
diff.externaltest demonstrates is recorded here as an openquestion — not asserted in either direction.
Related
that issue is about a single row asserting too little; this one is about
zero rows and about a row that is never read at all. fix(scripts): read revert subjects and assert the canary's replay attributions #2843 is the fix in
flight for silent-revert canary: verify_known_incidents proves only that a commit still fires, not which culprit or how many lines #2833 and does not touch either loop's read semantics.
predate it, finding 2's transcript is taken on its branch.