Fuzzing a PDF parser: three levels and a set of oracles
A PDF parser's input is whatever anyone chooses to hand it. "It does not crash" is the easy half of robustness. The interesting half is "it does not quietly give the wrong answer", and that needs a different kind of test.
Level one: generated broken documents
Random bytes make a poor PDF fuzzer. Almost every mutation produces a file that is rejected in the first few hundred bytes, so the deep code is never reached. An internal generator instead builds structurally valid skeletons and then corrupts them deliberately: truncation, bad cross-reference offsets, negative stream lengths, dangling and circular references, pathological nesting depth.
The point is to get past the front door. A file that is plausible enough to parse and wrong enough to matter exercises the recovery paths that real damaged documents hit, the ones behind a broken cross-reference table .
Level two: fuzz the layers separately
Whole-file fuzzing exercises decoders and sub-parsers only lightly. To reach the Flate decoder with an interesting input, the mutation has to survive everything upstream of it first. So the decoders and parsers are fuzzed directly, in isolation, where every input is a relevant one.
This is also where the resource limits come into play. Decoding a stream of a hundred gigabytes does not cause a crash; it causes a hang. A hang in a request handler causes an outage. A size limit for the converted stream converts this into a failed check.
Level three: oracles
The third level asks whether the answer was correct, which requires a property that remains consistent regardless of the input.
The first is determinism. Verifying the same document twice must produce the same result, byte for byte. It is the cheapest oracle and catches the most, such as map iteration order leaking into output and uninitialised state.
Re-verification is the second property. A conversion that is reported as valid must be re-verified independently as valid, which closes the loop between the verifier and the converter.
Then comes convergence. Conversion runs a verify/fix loop, so it has to terminate; fixing one issue must not reintroduce another issue indefinitely.
The fourth oracle is another implementation
Where a second implementation exists, it is the strongest oracle available. Every file in the veraPDF corpus is run through both gopdfrab and the veraPDF binary and the verdicts compared, so a divergence is a build failure rather than something a user discovers. That corpus and the Isartor suite both pass in full.
Why this matters for a validator
A validator sits on the untrusted side of a system by definition. You run it precisely on documents you do not trust yet. It has to be the thing that does not fall over, and it has to be right about files that are strange in ways no conformance corpus contains. Typed errors make the difference visible to callers: "this file is damaged" and "this file is valid PDF that fails PDF/A" are different results, not two shades of failure.