↑Supply Chain Security

Differential Fuzzing

title
Differential Fuzzing
type
concept
summary
Run the same input through two implementations of the same spec and treat disagreement as a bug; finds spec-conformance issues neither implementation would catch alone
tags
fuzzing, security, testing
created
2026-05-12
updated
2026-07-29

Most 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

A format decoder with a reference implementation is the canonical target, and as of 2026 no longer the only option for it: zstd-lean-proof-automation proves the absence of the counterexamples this technique hunts for, on the same class of code.

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'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.