Repository navigation
linearize: /S cannot point inside the page offset table's own header - #47
Merged
tannevaled merged 1 commit intoNov 14, 2026
Merged
Conversation
⭐ A LOWER BOUND ON /S THAT NEEDS NO DECODING, which is the part I had written
off. The previous commit refused only a negative /S and recorded the rest as
needing the decoded stream. Half of it does not.
F.3.6: the page offset hint table "shall be the first table in the stream and
shall start at offset 0". Table F.3 gives that table's header as exactly
thirteen items — 32, 32, 16, 32, 16, 32, 16, 32, 16, 16, 16, 16, 16 bits =
288 bits = 36 bytes — before a single per-page entry. /S names where the
SHARED OBJECT table begins, so it cannot be less than 36. That is arithmetic
on two sentences of the spec, not a measurement and not a guess.
⛔⛔ AND 36 IS EXACT, WHICH TWO INDEPENDENT WITNESSES SAY. Measured before
choosing it, across every producer available:
qpdf, table form /S 43 (decoded 71 bytes)
qpdf, object streams /S 43 (decoded 71 bytes)
Ghostscript /S 38 (decoded 67 bytes)
Ghostscript sits two bytes above the floor, so a bound eyeballed from qpdf
alone — 40, say — would have refused every file that tool writes. That is the
trap the /H exclusion walked into before a second producer existed, and this
time the measurement came first.
⭐ Then the mutation found the other witness. Raising the bound to 37 reddens
the boundary test AND the generated qpdf sweep's ONE-PAGE cases in all three
shapes: qpdf emits /S 36 exactly for a single-page document. So a real
producer sits ON the floor, 37 would break it, and the corpus proved the
off-by-one without being asked to.
Mutations, judged by cmd/failset on the failing NAMES:
36 -> 37 -> …/36_is_the_floor_and_not_past_it, …//S_-1, …//S_35,
AND TestCheckLinearizedAgreesWithQpdf… 1_pages x 3 shapes
36 -> 0 -> …//S_-1 and …//S_35 only — the floor case still passes,
correctly, because 0 accepts 36
Both directions, which is what an off-by-one needs: one mutation that refuses
too much and one that refuses too little, each red on different tests.
⚠ THE UPPER BOUND REMAINS OPEN and the reason is unchanged: /S indexes the
DECODED data, so bounding it above means decoding an attacker's bytes in the
cheap path — capped at 1 GiB by filter.go, which refuses a bomb but is three
orders of magnitude past anything a hint stream needs. That wants a size bound
derived from /N and the object count, which wants F.4's layouts. The ordering
is on #40.
gofmt clean, go vet clean, 0 uncovered blocks of 1812.
Refs #40.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
force-pushed
the
feat/the-shared-table-cannot-start-inside-the-header
branch
from
November 14, 2026 21:32
96033d6 to
189f8ba
Compare
tannevaled
deleted the
feat/the-shared-table-cannot-start-inside-the-header
branch
November 14, 2026 21:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⭐ A lower bound on
/Sthat needs no decoding — which is the part I had written off.The previous change refused only a negative
/Sand recorded the rest as requiring the decoded stream. Half of it does not.§F.3.6: the page offset hint table "shall be the first table in the stream and shall start at offset 0". Table F.3 gives that table's header as exactly thirteen items —
— before a single per-page entry.
/Snames where the shared object table begins, so it cannot be less than 36. That is arithmetic on two sentences of the spec: not a measurement, not a guess.⛔ And 36 is exact, which two independent witnesses say
Measured across every producer available before choosing the number:
/SGhostscript sits two bytes above the floor. A bound eyeballed from qpdf alone — 40, say — would have refused every file that tool writes. That is precisely the trap the
/Hexclusion walked into before a second producer existed, and this time the measurement came first.⭐ Then the mutation found the second witness. Raising the bound to 37 reddens the boundary test and the generated qpdf sweep's one-page cases in all three shapes: qpdf emits
/S 36exactly for a single-page document. So a real producer sits on the floor, 37 would break it, and the corpus proved the off-by-one without being asked to.Mutations
Judged by
cmd/failseton the failing test names:Both directions, which is what an off-by-one needs: one mutation refusing too much, one refusing too little, each red on different tests. A single mutation here would have left the boundary itself unproven.
⚠ The upper bound remains open
Unchanged reason:
/Sindexes the decoded data, so bounding it above means decoding an attacker's bytes in the cheap path — capped at 1 GiB byfilter.go, which refuses a bomb but is three orders of magnitude past anything a hint stream needs. That wants a size bound derived from/Nand the object count, which wants §F.4's layouts. The ordering is on #40.gofmt clean,
go vetclean, 0 uncovered blocks of 1812.Refs #40.
🤖 Generated with Claude Code