From 11a3ac92760f9cbda2f1fa2a9741c53e281ecd05 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 13 Nov 2026 17:51:40 +0100 Subject: [PATCH] README: the 32-bit refusal, and a count that three rounds of tests had falsified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⛔ A PUBLISHED COUNT DRIFTS AND NOTHING NOTICES. "The four cases that gate it" was true when written and had not been true for three review rounds — the overflow table alone carries five. The sentence is about the METHOD, which is what a reader needs and what does not go stale: a same-width edit of the committed qpdf fixture, so /L still matches and the front of the file is still correct. The number added nothing and was the only part that could rot. ⛔⛔ AND THE 32-BIT REFUSAL WAS NOT DOCUMENTED AT ALL, though it is public behaviour: a new error for values this build cannot address, on every entry including /P. A caller on a 32-bit build was previously handed a truncated value with no way to know. The README says why the hazard is not the value that becomes -1 — that one lands where a bound objects — but the one that lands on a PLAUSIBLE value: 2^32+127 truncating to 127, the fixture's own hint length. Both findings are recorded with their provenance, because they were not the same size and CI found only the smaller one. Refs #40. Co-Authored-By: Claude Opus 5 --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3faad6..ca58d9d 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ every parameter **points at what it claims to** — `/N` is the real page count, object, `/T` reaches the main cross-reference (either shape: Table F.1 gives it two meanings and both are real), `/E` is inside the file. -The four cases that gate it are files `Linearized` **accepts** and +What gates it are files `Linearized` **accepts** and `CheckLinearized` refuses: the committed qpdf fixture with one integer replaced by one of **the same width**, so `/L` still matches and nothing at the front of the file is wrong — what is wrong is that it describes a @@ -168,6 +168,25 @@ length, so a writer that miscomputes it passes here and fails qpdf. Computing it needs the first-page object set, which is the same partition the hint tables describe, so it waits for that work rather than being approximated now. +⛔⛔ **A value this build cannot address is refused, not truncated**, and the +same bytes therefore get the same answer on a 32-bit build as on a 64-bit one. +`int(Integer)` discards the high half where `int` is 32 bits, so a `/H` length +of 2³²+127 reads as **127** — not a wild value some bound would catch, but the +committed fixture's own hint length, which every later check then accepts. A +file would be judged sound on one machine and refused on another, with nothing +to say which had happened. Every integer the dictionary offers now goes through +one conversion that refuses what it cannot represent, `/P` included: it is +never used as an offset, but it is a public field, and a caller handed `0` for +2³² has no way to know. The refusal is `ErrLinearizationMalformed`. + +This was found twice over, which is worth saying because the two findings were +not the same size. CI's 386 and arm lanes went red first, on values that become +`-1` and happen to land where a bound objects — the small half. Review named +the hazard that has no symptom: the truncation that lands on a *plausible* +value. On a 64-bit build the refusal is unreachable from any file, so the limit +is a variable the tests shrink; otherwise the branch is exercised only by CI, +which is to say only after pushing. + ⛔ **The `/H` length is compared with the object exactly**, not within a window. A first version allowed the declared length to exceed the parsed extent by 0–2 bytes, reasoning that a writer might use CRLF; that accepts