Incremental save made /Prev chain walking load-bearing for every document,
not only saved ones: XRefTable::parse now follows offsets taken straight from
the file on every parse. Phase 6 established that code consuming untrusted
input needs corpus and fuzz coverage. That path had neither, so this adds it
and fixes what it found.
Corpus (7 new fixtures, generated by the checked-in script as usual):
- revisions/two.pdf, three.pdf: chained revisions that override a form
value. These are direct regression tests for the two bugs the previous
commit fixed. Before it, two.pdf read back as "first" rather than
"second", because find_xref_start never matched its own keyword and fell
through to the oldest section in the file.
- revisions/added_page.pdf: a revision that rewrites /Pages, so the newer
definition must win for structure as well as for values.
- malformed/prev_loop.pdf, prev_out_of_range.pdf, prev_negative.pdf and
prev_chain_bomb.pdf: the hostile shapes.
Two robustness defects found by those fixtures:
- A broken /Prev orphaned every object the unreachable sections defined,
even though the bytes were still in the file, so a document with one bad
offset failed to open at all. The chain now sets a recovered flag and
sweeps the file for object headers, filling only genuine gaps: entries a
parsed section supplied always win, because those reflect the document's
own view of which revision is current, and scanning cannot tell newer
from older.
- A negative /Prev was filtered to None, which silently ended the chain as
though the file had no history. It is now treated as a broken link and
triggers the same recovery.
Also caps the chain at 64 revisions. A legitimate document has a handful; a
file with thousands is an attack, not a history. prev_chain_bomb.pdf asserts
the cap holds and that parsing stays fast.
The recovered flag is public so a caller can distinguish a cleanly parsed
document from a salvaged one rather than being handed a guess silently. A
test asserts it stays false for healthy files, or it would mean nothing.
Fuzzing: adds parse_revision_chain, which splices fuzzer input onto a valid
base document so the fuzzer spends its time on chain shapes rather than on
rediscovering PDF syntax. Run for real, not merely compile-checked:
parse_revision_chain 1,926,164 runs
parse_xref 1,387,713 runs
parse_document 1,279,328 runs
No crashes. The two re-run targets cover the file this commit changes.
One fixture-generator bug fixed on the way: the helper that reads a file's
startxref took the first token after rfind without skipping the keyword,
producing a startxref that pointed at its own text.
Validation:
TEST_TARGET=pdf ./tools/test-rust-clean.sh (318 tests)
TEST_TARGET=pdf-ui ./tools/test-rust-clean.sh (362 tests, 6 ignored)
Both rustfmt and clippy -D warnings clean.