Fuzz Testing Guide: Finding Bugs Computers Are Better at Finding Than Humans

Fuzz Testing Guide: Finding Bugs Computers Are Better at Finding Than Humans

Fuzz testing (fuzzing) automatically generates random, malformed, or unexpected inputs and feeds them to your program looking for crashes, hangs, memory corruption, and assertion violations. Modern coverage-guided fuzzers like AFL++ and LibFuzzer generate millions of test cases per second guided by code coverage feedback and have found thousands of CVEs in widely-used software.

What Fuzz Testing Is

A fuzzer runs in a tight loop:

  1. Generate an input (random or based on a corpus)
  2. Feed it to the target program
  3. Observe the result: crash? hang? memory error? assertion fail?
  4. If interesting output: save the input to the corpus
  5. Repeat at millions of iterations per second

Coverage-guided fuzzing tracks which code paths each input exercises. When an input reaches a new code path, it's added to the corpus and used as a mutation seed — the fuzzer explores outward from what works.

What Fuzzing Finds

Fuzzing excels at finding:

Memory safety issues (C/C++):

  • Buffer overflows
  • Use-after-free
  • Heap corruption
  • Integer overflows leading to buffer over-reads

Logic errors:

  • Crashes on unexpected input
  • Assertion failures
  • Infinite loops on certain inputs

Security vulnerabilities:

  • XML/JSON parser bombs
  • Format string vulnerabilities
  • Path traversal via unusual encodings

Go Fuzzing (Built-in since Go 1.18)

func FuzzParseConfig(f *testing.F) {
    f.Add("key=value")
    f.Add("")

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseConfig(input)
        if err == nil {
            encoded := result.Encode()
            _, err2 := ParseConfig(encoded)
            if err2 != nil {
                t.Errorf("Round-trip failed: %q -> %q -> %v", input, encoded, err2)
            }
        }
    })
}
go test -fuzz=FuzzParseConfig -fuzztime=60s

Python Fuzzing with Atheris

import atheris
import sys
import json

def TestOneInput(data: bytes):
    fdp = atheris.FuzzedDataProvider(data)
    text = fdp.ConsumeUnicodeNoSurrogates(len(data))
    try:
        result = json.loads(text)
        re_encoded = json.dumps(result)
        json.loads(re_encoded)
    except (json.JSONDecodeError, ValueError):
        pass

atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()

Java Fuzzing with Jazzer

public class FuzzTarget {
    public static void fuzzerTestOneInput(FuzzedDataProvider data) {
        String input = data.consumeRemainingAsString();
        try {
            MyXmlParser.parse(input);
        } catch (MyParseException e) {
            // Expected
        }
    }
}

Differential Fuzzing

Test two implementations of the same spec and flag disagreements:

func FuzzHTTPParsers(f *testing.F) {
    f.Fuzz(func(t *testing.T, input []byte) {
        result1, err1 := parser1.Parse(input)
        result2, err2 := parser2.Parse(input)

        if (err1 == nil) != (err2 == nil) {
            t.Errorf("Parsers disagree: parser1=%v parser2=%v on %q", err1, err2, input)
        }
    })
}

Integrating Fuzzing into CI

Seed corpus regression (every build):

go test -run=FuzzParseConfig  # fast: only runs seed corpus

Time-boxed fuzzing on PRs:

- name: Fuzz test (5 minutes)
  run: go test -fuzz=FuzzParseConfig -fuzztime=300s

Extended fuzzing (weekly):

on:
  schedule:
    - cron: '0 3 * * 0'
jobs:
  fuzz:
    steps:
      - run: go test -fuzz=FuzzParseConfig -fuzztime=3600s

Always Use Sanitizers

gcc -fsanitize=address,undefined -g -O1 target.c -o target

Fuzzing without AddressSanitizer means memory errors go undetected.

Tools by Language

Tool Languages Type
AFL++ C/C++ Coverage-guided
LibFuzzer C/C++ Coverage-guided
go-fuzz / native Go Coverage-guided
Atheris Python Coverage-guided
Jazzer Java/JVM Coverage-guided
OWASP ZAP Web apps Black-box

Read more

Start now free