# Differential Fuzzing

Most [[coverage-guided-fuzzing|coverage-guided fuzzing]] looks for crashes — segfaults, panics, asserts, sanitizer trips. Differential fuzzing looks for *disagreement*: feed the same input to two independent implementations of the same specification, compare their outputs, treat any divergence as a bug. The implementations cross-validate each other; neither has to crash for the test to fail.

The technique is the right one when:

- The spec is the contract under test, not any single implementation
- The cost of inter-implementation divergence is high — consensus systems, cryptographic protocols, compilers producing reproducible binaries
- There are at least two reasonably-mature implementations in the ecosystem to compare

## Where it lives in practice

- **Ethereum L1/L2 clients** — geth vs nethermind vs reth, op-node vs Kona, EVM vs Revm. A state-root mismatch on the same block is a chain split.
- **TLS** — OpenSSL vs BoringSSL vs Rustls, sometimes also vs the wire-format spec
- **JSON / serialization** — encoders' acceptance of "exotic" inputs (duplicate keys, leading zeros, NaN) often diverges, sometimes exploitably
- **Compilers** — Csmith and Yarpgen drive differential fuzzing across GCC/Clang/MSVC
- **SQL engines** — SQLite as oracle vs Postgres/MySQL for the same query
- **Protocol parsers** — HTTP request smuggling research used differential parsing between front-end proxies and back-end servers

## Combination with grammar-based fuzzing

[[grammar-based-fuzzing]] is what makes differential fuzzing practical at scale: both implementations need to actually parse the input far enough to produce output, so the fuzzer can't waste cycles on inputs both reject at byte zero. A grammar that produces "things that look like a valid transaction" exercises the post-parse semantic disagreements, which is where consensus bugs live.

## Cases from gosentry's early campaigns

[[gosentry-go-fuzzing-fork|gosentry]]'s blog post lists four early findings, all consensus-shaped:

- Optimism / Kona vs op-node disagreed on Brotli channel handling
- Frame parsing diverged from the Optimism spec
- Revm's failed-deposit handler didn't bump the nonce — state-root mismatch against other Ethereum clients
- Optimism / Kona panicked on unknown batch types instead of rejecting

Each one is a spec interpretation gap, not an implementation crash.
