⚙ Application Security

Fuzzing Explained: Finding Bugs Automatically

Fuzzing is an automated software-testing technique that feeds a program large volumes of random, malformed, or unexpected inputs to discover crashes, hangs, and security vulnerabilities. Because a fuzzer can generate and test inputs far faster and more creatively than any human, it excels at exposing edge cases that developers never anticipated. Fuzzing has become a cornerstone of modern security testing, credited with finding countless serious bugs in compilers, browsers, network stacks, and file parsers.

How Fuzzing Works

At its core, a fuzzer runs a target program in a loop: generate an input, feed it to the program, and watch for abnormal behavior such as a crash, an assertion failure, a memory error, or a timeout. When the program misbehaves, the fuzzer saves the offending input so an engineer can reproduce and diagnose it.

The reason fuzzing is so productive is that programmers naturally test the inputs they expect. Attackers, by contrast, supply inputs designed to break assumptions. A fuzzer approximates the attacker's mindset mechanically, probing the vast space of malformed inputs — truncated files, enormous values, invalid encodings, deeply nested structures — that manual tests rarely cover.

Types of Fuzzers

Fuzzers are categorized along two important axes.

How They Create Inputs

  • Mutation-based fuzzers start from a corpus of valid seed inputs and randomly mutate them — flipping bits, inserting bytes, or splicing samples together. They require little setup but may struggle to satisfy strict input formats.
  • Generation-based fuzzers build inputs from scratch according to a model or grammar of the expected format. They produce more valid, structurally deep inputs but require effort to write the model.

How Much They Know About the Target

  • Black-box fuzzers observe only the input and the program's external behavior.
  • White-box fuzzers use program analysis, sometimes including symbolic execution, to reason about which inputs reach which code.
  • Gray-box fuzzers take a lightweight middle path, using code-coverage feedback to guide mutation without heavyweight analysis. This category, known as coverage-guided fuzzing, has proven the most effective in practice.

Coverage-Guided Fuzzing

Coverage-guided fuzzing is the technique behind widely used tools such as AFL and libFuzzer. The key idea is a feedback loop: the target is compiled with instrumentation that reports which code branches each input exercises. When a mutated input reaches a new branch, the fuzzer treats it as interesting and keeps it in the corpus for further mutation.

The loop looks like this:

  1. Pick an input from the corpus.
  2. Mutate it to produce a new candidate.
  3. Run the target and record the code coverage.
  4. If the candidate hit new coverage, add it to the corpus; otherwise discard it.
  5. If the target crashed, save the input as a finding, then repeat.

This evolutionary process lets the fuzzer "learn" the input format by discovery. Starting from a trivial seed, it can gradually build inputs that pass length checks, magic-number checks, and parsing stages to reach deep, rarely tested code — all without a human specifying the format.

Amplifying Fuzzing with Sanitizers

A crash is an obvious signal, but many serious bugs — such as a small out-of-bounds read — do not crash on their own. Sanitizers dramatically increase a fuzzer's sensitivity by turning subtle undefined behavior into loud, immediate failures.

  • AddressSanitizer (ASan) detects buffer overflows and use-after-free.
  • UndefinedBehaviorSanitizer (UBSan) catches integer overflow and other undefined operations.
  • MemorySanitizer (MSan) flags reads of uninitialized memory.

Fuzzing is therefore a powerful partner to memory safety efforts: the fuzzer generates the triggering inputs, and the sanitizer ensures the resulting memory error is actually detected rather than passing silently.

Writing a Fuzz Target

Most coverage-guided fuzzers drive a small harness function that hands raw bytes to the code under test. A minimal libFuzzer-style harness looks like this:

// Compiled with instrumentation and a sanitizer, then run in a loop.
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    parse_untrusted_format(data, size);  // the code we want to stress
    return 0;                            // non-zero is reserved
}

Effective harnesses share a few traits. They target code that parses untrusted input, since that is where attacker-controlled data enters. They are fast and deterministic, because the fuzzer runs them millions of times. And they start from a small seed corpus of representative valid inputs to give coverage-guided mutation a head start.

Fitting Fuzzing into Development

Fuzzing is most valuable when it runs continuously rather than as a one-time exercise. Teams integrate it in several ways:

  • Continuous fuzzing runs harnesses around the clock so new code is stress-tested as it lands, and large open-source projects benefit from shared continuous-fuzzing infrastructure.
  • Regression fuzzing replays the saved corpus and past crash inputs on every build to prevent old bugs from returning.
  • Triage and deduplication group crashes by root cause so engineers are not overwhelmed by many inputs that trigger the same bug.

Fuzzing complements, rather than replaces, other techniques like SAST and DAST: static analysis reasons about code without running it, while fuzzing exercises the real program to confirm exploitable behavior.

Key Takeaways

  • Fuzzing automatically generates malformed inputs to find crashes and security bugs that manual testing misses.
  • Fuzzers differ by how they create inputs (mutation vs generation) and how much they know about the target (black-, gray-, or white-box).
  • Coverage-guided fuzzing uses instrumentation feedback to evolve inputs that reach deep code, making it the most effective common approach.
  • Pairing fuzzers with sanitizers turns silent memory errors into detectable crashes, greatly increasing the bugs found.
  • Fuzzing delivers the most value when run continuously with good harnesses, a seed corpus, and automated crash triage.
fuzzingfuzz-testingsecurity-testingautomationappsec

Frequently asked questions

What is fuzzing?

Fuzzing is an automated software-testing technique that feeds a program large volumes of random, malformed, or unexpected inputs to discover crashes, hangs, and security vulnerabilities. Because a fuzzer can generate and test inputs far faster and more creatively than any human, it excels at exposing edge cases that developers never anticipated. It has become a cornerstone of security testing, credited with finding serious bugs in compilers, browsers, network stacks, and file parsers.

How does fuzzing work?

A fuzzer runs a target program in a loop: it generates an input, feeds it to the program, and watches for abnormal behavior such as a crash, an assertion failure, a memory error, or a timeout. When the program misbehaves, the fuzzer saves the offending input so an engineer can reproduce and diagnose it. It is productive because it mechanically approximates an attacker's mindset, probing the vast space of malformed inputs that manual tests rarely cover.

What is coverage-guided fuzzing?

Coverage-guided fuzzing is the technique behind widely used tools such as AFL and libFuzzer. The target is compiled with instrumentation that reports which code branches each input exercises, and when a mutated input reaches a new branch the fuzzer treats it as interesting and keeps it in the corpus for further mutation. This evolutionary feedback loop lets the fuzzer effectively learn the input format and reach deep, rarely tested code without a human specifying the format, which makes it the most effective common approach.

What is the difference between mutation-based and generation-based fuzzing?

Mutation-based fuzzers start from a corpus of valid seed inputs and randomly mutate them by flipping bits, inserting bytes, or splicing samples together, which requires little setup but may struggle to satisfy strict input formats. Generation-based fuzzers instead build inputs from scratch according to a model or grammar of the expected format, producing more valid, structurally deep inputs at the cost of the effort to write the model.

Why use sanitizers with fuzzing?

Many serious bugs, such as a small out-of-bounds read, do not crash on their own, so a fuzzer watching only for crashes would miss them. Sanitizers turn subtle undefined behavior into loud, immediate failures: AddressSanitizer detects buffer overflows and use-after-free, UndefinedBehaviorSanitizer catches integer overflow and other undefined operations, and MemorySanitizer flags reads of uninitialized memory. Pairing fuzzers with sanitizers greatly increases the number of bugs found.

How do I write a good fuzz target?

Write a small harness function that hands raw bytes to the code under test, and point it at code that parses untrusted input, since that is where attacker-controlled data enters. Make the harness fast and deterministic, because the fuzzer will run it millions of times, and provide a small seed corpus of representative valid inputs to give coverage-guided mutation a head start. Running the harness continuously, with regression replay and crash deduplication, delivers far more value than a one-time exercise.

Try it hands-on

K0G is an open toolkit of browser-based security utilities — hashing, encoding, JWT, certificates, crypto and more, all running locally in your browser.

Explore the tools →