Skip to content

buildcheck: catch a patch hunk header that lies about its size - #495

Closed
bernardladenthin wants to merge 1 commit into
mainfrom
claude/patch-hunk-guard
Closed

bernardladenthin wants to merge 1 commit into
mainfrom
claude/patch-hunk-guard

Conversation

@bernardladenthin

Copy link
Copy Markdown
Owner

The guard for the regression that put 23 jobs of one Publish run in the red — and for the reason
it got through in the first place.

Why nothing caught it

git apply --check validates that a hunk's context applies, not that its declared line count
matches its body.
A header that under-counts passes the check, and git apply then writes only
the declared number of lines — silently truncating the file.

Editing a comment inside the new file patches/0017 adds left its header at @@ -0,0 +1,25 @@ for
27 lines, so prefetch.h arrived without its closing brace, every translation unit including it
continued inside an unclosed function, and quants.c failed 19 × function definition is not allowed here on every platform.

Nothing existing could see it:

why not
git apply --check checks context, not counts — reported the patch as fine
apply-llama-patches.cmake a truncated file is a successful apply as far as git is concerned
verify-patches-applied.sh asserts the patch is in the stamp and the tree is dirty — both true

The check

Static, so it runs in code-style (the first minutes of a run) rather than after a build, and
locally before a push:

python3 .github/check-patches.py
11 patches checked, 0 hunk size mismatch(es)

The algorithm is the part worth reading, because two obvious forms are wrong and one of them
accepts exactly this bug.
Counting body lines until the next delimiter over-counts trailing
context and passes a header that under-counts. Only checking that a body is not SHORT misses it
too. The correct form: consume exactly as many lines as the header declares, then require a
delimiter to follow — a body that ends early is short, a body that continues is under-counted.

Both messages say what git apply would do about it, and the under-count one names the first line
that would be dropped.

Tests

11 unit tests: both directions, a wrong old-side count, the @@ -5 +5 @@ single-line shorthand,
\ No newline at end of file counting for neither side, prose and a git signature below the last
hunk (this repository's patches carry long descriptions above the diff), and that every hunk of a
multi-hunk patch is checked. One runs the check over the repository's own 11 patches.

Verified red/green on the real thing: reconstructing the broken header reports

0017…patch:78: hunk declares -0 +25 but its body continues at line 104
('+    return (const char *) base + byte_offset;') -- the header UNDER-counts, so
`git apply` writes a TRUNCATED file and `--check` does not notice

which is precisely the line that went missing.

Full buildcheck suite: 114 tests green (103 + 11).

Not shared

patches.py is jllama-only — only this repository has llama/patches/ — so it is deliberately not
in .github/shared-files.sha256 and not in __init__.py's shared list.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G

The guard for the regression that put 23 jobs of one Publish run in the red, and the reason it got
through: `git apply --check` validates that a hunk's CONTEXT applies, not that its declared line
count matches its body. A header that under-counts therefore passes the check, and `git apply`
writes only the declared number of lines -- silently truncating the file. Editing a comment inside
the new file `patches/0017` adds left its header at `+1,25` for 27 lines, so prefetch.h arrived
without its closing brace, every translation unit including it continued inside an unclosed
function, and quants.c failed 19x with "function definition is not allowed here" on every platform.

Nothing existing could see it. The applier treats a truncated file as a successful apply, and
verify-patches-applied.sh asserts only that every patch is in the stamp and that the tree is dirty
-- both true. So this is a STATIC check of the patch files, wired into `code-style` (the first
minutes of a run) rather than after a build, and runnable before a push:

    python3 .github/check-patches.py

The algorithm is the part worth reading, because two obvious forms of it are wrong and one of them
accepts exactly this bug. Counting body lines until the next delimiter over-counts trailing context
AND passes a header that under-counts. Only checking that a body is not SHORT misses it as well.
The correct form is: consume exactly as many lines as the header declares, then require that a
delimiter follows -- a body that ends early is short, a body that continues is under-counted, and
both messages say what `git apply` would do about it.

11 unit tests cover both directions, a wrong old-side count, the `@@ -5 +5 @@` single-line
shorthand, "\ No newline at end of file" counting for neither side, prose and a git signature below
the last hunk (this repository's patches carry long descriptions), and that every hunk of a
multi-hunk patch is checked. One of them runs the check over the repository's own 11 patches, which
pass. Full buildcheck suite: 114 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
@bernardladenthin

Copy link
Copy Markdown
Owner Author

Closing as a duplicate of #493, which is merged. The other session built the same guard independently and arrived at the same algorithm -- consume the declared count, then require a delimiter -- so it catches both directions, including the under-count that truncates the file. Its version is better in two respects: it tracks the +++ b/<file> target so a message names the affected file, and it reconstructs the real broken 0017 as a test case rather than a synthetic one.

Checked rather than assumed before closing: the four test cases this PR had and #493 does not (the @@ -5 +5 @@ single-line shorthand, a wrong old-side count, a multi-hunk patch whose second hunk is broken, and the empty-directory exit code) all pass against the merged implementation. So they are test hardening, not a gap -- not worth a third overlapping PR.

@bernardladenthin
bernardladenthin deleted the claude/patch-hunk-guard branch October 9, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant