Skip to content

Fuzz Testing

gal kahana edited this page May 29, 2026 · 1 revision

PDF-Writer ships a set of libFuzzer harnesses targeting the parser/decoder code in the library and its bundled dependencies. They're opt-in: a single CMake flag wires the instrumentation into every binary, and the harnesses build alongside the regular test executable. Maintainers run them occasionally to surface crash, UB and memory bugs that the unit tests don't reach.

Prerequisites

  • LLVM Clang (Apple Clang on macOS doesn't ship libFuzzer)
  • LLD linker
  • Linux is the simplest target. On macOS use Homebrew:
    brew install llvm lld
    
    then use /opt/homebrew/opt/llvm/bin/clang and /opt/homebrew/opt/llvm/bin/clang++ as the compilers.

Build

mkdir build
cd build
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DBUILD_FUZZING_HARNESS=ON ..
cmake --build . -- -j$(nproc)

The BUILD_FUZZING_HARNESS=ON flag is applied globally: PDFWriter and every bundled dependency (zlib, freetype, libpng, libjpeg, libtiff, libaesgm) is compiled with -fsanitize=fuzzer-no-link,address so libFuzzer has real coverage signal across the whole library — not just the harness body. Each harness binary additionally links -fsanitize=fuzzer to pull in the libFuzzer runtime.

Harnesses

Each harness targets one parser or decoder. Run any of them by pointing at its seed corpus directory:

Harness Target Corpus Dict
PDFParserFuzzingHarness PDFParser (deep — page tree, indirect objects, stream decoding) Materials/fuzzing/MinimalFuzzingCorpus pdf.dict
JPEGImageParserFuzzingHarness JPEGImageParser::Parse Materials/fuzzing/JPEGCorpus jpeg.dict
TIFFImageHandlerFuzzingHarness PDFWriter::CreateFormXObjectFromTIFFStream (full TIFF → PDF pipeline) Materials/fuzzing/TIFFCorpus
InputFlateDecodeStreamFuzzingHarness InputFlateDecodeStream (FlateDecode / zlib filter) Materials/fuzzing/FlateCorpus
InputLZWDecodeStreamFuzzingHarness InputLZWDecodeStream (LZWDecode filter) Materials/fuzzing/LZWCorpus
InputAESDecodeStreamFuzzingHarness InputAESDecodeStream / InputAESDecodeStreamSSL (chosen at build time by USE_OPENSSL_AES) Materials/fuzzing/AESCorpus
OpenTypeFileInputFuzzingHarness OpenTypeFileInput::ReadOpenTypeFile Materials/fuzzing/OpenTypeCorpus
CFFFileInputFuzzingHarness CFFFileInput::ReadCFFFile Materials/fuzzing/CFFCorpus

The TIFF and AES harnesses are conditional on their feature flags: TIFFImageHandlerFuzzingHarness is not built when PDFHUMMUS_NO_TIFF is set, and InputAESDecodeStreamFuzzingHarness picks between the OpenSSL-backed and libaesgm-backed AES implementation depending on USE_OPENSSL_AES.

The AES harness uses a fixed 16-byte key (0..15) — it fuzzes for memory-safety bugs in the AES path (IV handling, padding, short ciphertext), not for crypto correctness.

The deep PDF parser harness caps per-iteration work at 1000 pages, 1000 objects, and 1 MB per decoded stream so libFuzzer can iterate at thousands of inputs per second. The production library validates these values independently; the caps are throughput tuning, not security bounds.

Run

./PDFWriterTesting/PDFParserFuzzingHarness \
  -dict=../PDFWriterTesting/Materials/fuzzing/pdf.dict \
  ../PDFWriterTesting/Materials/fuzzing/MinimalFuzzingCorpus

./PDFWriterTesting/JPEGImageParserFuzzingHarness \
  -dict=../PDFWriterTesting/Materials/fuzzing/jpeg.dict \
  ../PDFWriterTesting/Materials/fuzzing/JPEGCorpus

./PDFWriterTesting/TIFFImageHandlerFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/TIFFCorpus

./PDFWriterTesting/InputFlateDecodeStreamFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/FlateCorpus

./PDFWriterTesting/InputLZWDecodeStreamFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/LZWCorpus

./PDFWriterTesting/InputAESDecodeStreamFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/AESCorpus

./PDFWriterTesting/OpenTypeFileInputFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/OpenTypeCorpus

./PDFWriterTesting/CFFFileInputFuzzingHarness \
  ../PDFWriterTesting/Materials/fuzzing/CFFCorpus

Dict files give libFuzzer a vocabulary of format-specific anchor tokens (PDF keywords, JPEG markers) to use as mutation building blocks. Harnesses without a dict still work; adding one would help libFuzzer reach deeper code faster.

libFuzzer prints periodic stats (cov: for edges covered, ft: for features, corp: for active corpus size, exec/s: for iterations per second). Crashes are saved to crash-* files in the working directory for analysis. Stop with Ctrl-C; libFuzzer writes a final summary.

Advanced fuzzing with AFL++

For multi-core scaling and dictionary support beyond what libFuzzer provides, compile with AFL++:

cmake -DCMAKE_C_COMPILER=afl-clang-fast \
  -DCMAKE_CXX_COMPILER=afl-clang-fast++ -DBUILD_FUZZING_HARNESS=ON ..

Then run:

afl-fuzz -i ../PDFWriterTesting/Materials/fuzzing/MinimalFuzzingCorpus \
  -o fuzzing-out -x ../PDFWriterTesting/Materials/fuzzing/pdf.dict \
  -t 50 -- ./PDFParserFuzzingHarness

Cross-link

For static instrumentation of the existing test suite (instead of random-input fuzzing), see the Sanitizer Builds Of PDFWriter page. The two are complementary — sanitizers + fuzzing together is the gold standard, and the harness build already enables -fsanitize=fuzzer-no-link,address. UBSan composes on top with -DPDFHUMMUS_SANITIZER=undefined.

Clone this wiki locally