|
| 1 | +# ADR 0020: Jazzer fuzz testing infrastructure |
| 2 | + |
| 3 | +- **Status:** Proposed |
| 4 | +- **Date:** 2026-07-12 |
| 5 | +- **Deciders:** project maintainer |
| 6 | +- **Related:** [ADR 0003 — VortexException sanitization](0003-vortex-exception-sanitization.md), |
| 7 | + [ADR 0004 — Resource caps and `ReadOptions`](0004-resource-caps-read-options.md), |
| 8 | + [TODO.md §Security](../TODO.md) |
| 9 | + |
| 10 | +## Context |
| 11 | + |
| 12 | +The reader's security contract (TODO.md §Security) requires every malformed input to fail as |
| 13 | +`VortexException`, never a raw JVM crash class (`ArrayIndexOutOfBoundsException`, |
| 14 | +`OutOfMemoryError`, `StackOverflowError`, a raw FlatBuffer/Protobuf parser exception). Per-encoding |
| 15 | +adversarial unit tests cover known gotchas by hand (non-monotonic offsets, out-of-range codes, |
| 16 | +negative dimensions, …), but hand-written cases only exercise inputs someone already thought of — |
| 17 | +they don't explore the input space the way a coverage-guided fuzzer does. |
| 18 | + |
| 19 | +[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer) is a coverage-guided fuzzer for the |
| 20 | +JVM built on libFuzzer, with first-class JUnit 5 integration (`@FuzzTest`) and is also a supported |
| 21 | +OSS-Fuzz engine. |
| 22 | + |
| 23 | +## Decision |
| 24 | + |
| 25 | +Adopt Jazzer via the `com.code-intelligence:jazzer-junit` test dependency, in two modes: |
| 26 | + |
| 27 | +- **regression** — `./mvnw test` replays the saved corpus and crash reproducers, so every |
| 28 | + fuzz-discovered bug becomes a permanent part of the ordinary test run. |
| 29 | +- **fuzz** — `JAZZER_FUZZ=1`, run on a nightly profile; this is the mode that actually explores |
| 30 | + new inputs. It must not run on every normal build. |
| 31 | + |
| 32 | +### Seed corpus |
| 33 | + |
| 34 | +Existing `.vortex` integration fixtures seed `reader/src/test/resources/fuzz-corpus/full-file/`. |
| 35 | +A small extraction tool walks those fixtures and dumps each segment to |
| 36 | +`core/src/test/resources/fuzz-corpus/<encoding>/`, producing per-encoding sub-corpora without |
| 37 | +hand-crafting inputs. |
| 38 | + |
| 39 | +### Fuzz targets |
| 40 | + |
| 41 | +- `VortexReader.open(byte[])` — full-file parse path. |
| 42 | +- `PostscriptParser.parseBlobs` — postscript/footer parsing boundary. |
| 43 | +- One `@FuzzTest` per encoding `Encoding.decode` — per-encoding decode boundary. |
| 44 | + |
| 45 | +Crash oracle: `ignore = {VortexException.class}` — anything else is a real finding, per the |
| 46 | +security contract in TODO.md §Security. |
| 47 | + |
| 48 | +### Differential fuzz (Java vs Rust) |
| 49 | + |
| 50 | +Round-trip random bytes through Java decode and `vortex-jni`; assert both throw or both return |
| 51 | +identical row count + values. Reuses the `RustWritesJavaReadsIntegrationTest` harness — this mode |
| 52 | +catches semantic divergence from the Rust reference, not just crashes. |
| 53 | + |
| 54 | +### OSS-Fuzz submission |
| 55 | + |
| 56 | +Once the corpus and targets stabilize, submit the project to OSS-Fuzz. Jazzer is a first-class |
| 57 | +OSS-Fuzz engine, giving free continuous fuzzing beyond what CI runs. |
| 58 | + |
| 59 | +## Consequences |
| 60 | + |
| 61 | +### Positive |
| 62 | + |
| 63 | +- Explores the untrusted-input space beyond hand-written adversarial tests; the per-encoding |
| 64 | + gotcha list in TODO.md §Security becomes a floor, not a ceiling. |
| 65 | +- Crash reproducers become permanent regression tests via the JUnit 5 integration. |
| 66 | +- Differential mode catches semantic divergence from the Rust reference, not just crashes. |
| 67 | +- OSS-Fuzz gives continuous fuzzing without maintaining dedicated infrastructure. |
| 68 | + |
| 69 | +### Negative |
| 70 | + |
| 71 | +- New test dependency (`jazzer-junit`) and a nightly CI profile to maintain. |
| 72 | +- The fuzz corpus needs periodic curation as encodings evolve, or it silently stops finding |
| 73 | + anything new. |
| 74 | + |
| 75 | +### Risks to manage |
| 76 | + |
| 77 | +- `JAZZER_FUZZ=1` must stay opt-in (nightly profile only) — active fuzzing on every |
| 78 | + `./mvnw test` would make the normal build slow and non-deterministic. |
| 79 | +- Crash reproducers must be triaged into either a real fix or an explicit `VortexException` |
| 80 | + classification before landing in the corpus, or noise accumulates. |
| 81 | + |
| 82 | +## Alternatives considered |
| 83 | + |
| 84 | +- **Hand-written adversarial tests only (status quo)** — cheap, but bounded by what a human |
| 85 | + enumerates; the per-encoding gotcha list in TODO.md §Security exists precisely because this |
| 86 | + approach misses things. |
| 87 | +- **AFL/libFuzzer via a custom JNI harness** — more mature native fuzzing ecosystem, but requires |
| 88 | + building and maintaining a harness that bridges into JVM bytecode. Jazzer already does this and |
| 89 | + ships JUnit 5 integration out of the box. |
| 90 | + |
| 91 | +## References |
| 92 | + |
| 93 | +- [Jazzer](https://github.com/CodeIntelligenceTesting/jazzer) |
| 94 | +- [TODO.md §Security](../TODO.md) |
| 95 | +- [ADR 0003 — VortexException sanitization](0003-vortex-exception-sanitization.md) |
| 96 | +- [ADR 0004 — Resource caps and `ReadOptions`](0004-resource-caps-read-options.md) |
0 commit comments