Map
โ†‘Supply Chain Security

Coverage-Guided Fuzzing

Wiki conceptfuzzingsecuritytesting โ†ณ show in map Markdown
title
Coverage-Guided Fuzzing
type
concept
summary
The dominant modern fuzzing algorithm โ€” evolutionary loop over a corpus of test cases, fitness measured by code coverage, mutated offspring kept when they reach new coverage
tags
fuzzing, security, testing
created
2026-05-12
updated
2026-05-12

The default algorithm of any modern fuzzer worth using. Treat the set of test cases as an evolving population, the SUT's execution paths as the fitness landscape, and "reached new coverage" as the selection criterion. Test cases that exercise new code stay in the corpus and get mutated further; offspring that don't add coverage are discarded.

From the Trail of Bits Testing Handbook:

fn fuzz(corpus) {
  let bug_set = [];
  while let Some(test_case) = schedule(corpus) {
    let offspring = mutate(test_case);
    let observations = execute(offspring);

    if (is_interesting(observations)) { corpus.append(offspring); }
    if (is_bug(observations))         { bug_set.append(offspring); }
  }
  return bug_set;
}

Four customization points, in roughly increasing order of how much they matter to a campaign:

  • mutate โ€” bit flips, byte inserts/deletes, splices between corpus entries, dictionary-token insertion. Most fuzzers ship a fixed stack of mutators; advanced engines let you compose your own.
  • is_interesting โ€” almost always "did the run hit an edge not in the coverage map yet"; some engines also track comparison-operand state (libFuzzer's value-profile) or function-call-context coverage
  • schedule โ€” picks which corpus entry to mutate next. AFL's "favored seeds" prioritization is the canonical example; LibAFL exposes scheduling as a trait
  • execute โ€” runs the SUT under instrumentation; the engine's I/O hot path lives here

Why coverage works as fitness

Coverage is a cheap proxy for "the input exercises code the previous corpus did not." That's almost the right thing โ€” the true objective is "find inputs that reveal bugs," but bugs cluster in under-exercised code, and edge coverage correlates closely enough with reachability that the heuristic produces the bug counts the field is famous for. It does mean coverage-guided fuzzing systematically misses bugs in fully covered code (e.g., subtle data-dependent bugs in a path the corpus reaches but doesn't stress).

What instrumentation makes this work

The fuzzer needs to know which edges executed. Two paths:

  • Compile-time โ€” -fsanitize=fuzzer (Clang) or AFL's wrapper compilers insert a per-edge counter. Cheap at runtime, requires building the SUT.
  • Binary-only โ€” QEMU mode, Intel PT, Frida-mode. Slower (10-100ร—), but doesn't need source. Used when fuzzing closed-source targets or compiled deps.

How this composes with other techniques

  • grammar-based-fuzzing โ€” substitutes mutate for a grammar-aware tree mutator; coverage feedback still works the same
  • structure-aware-fuzzing โ€” wraps the input in a decoder; the corpus is bytes, the SUT sees typed values
  • differential-fuzzing โ€” replaces is_bug with "implementation A's output โ‰  implementation B's output"; coverage still drives is_interesting

Engines

  • libFuzzer โ€” in-process, fastest, requires source. The LLVMFuzzerTestOneInput API.
  • AFL++ โ€” out-of-process, broader sanitizer/coverage feature set, the descendant of original AFL
  • LibAFL โ€” library of composable parts; build your own fuzzer
  • Honggfuzz โ€” Google fuzzer with PT/Intel BTS and persistent mode
  • Engine-specific specializations โ€” kAFL (kernel fuzzing via Intel PT in KVM), Syzkaller (Linux syscall fuzzing), AFLNet (stateful protocol fuzzing)