CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/coverage-guided-fuzzing

Coverage-guided fuzzing across every mainstream engine - libFuzzer (C/C++ in-process), AFL++ (out-of-process, QEMU mode for closed-source binaries), cargo-fuzz (Rust), Go native fuzzing (go test -fuzz), Atheris (Python), and Jazzer (JVM, @FuzzTest). Body covers choosing the right fuzzer for the language and build type (the routing tree) plus the engine-generic workflow: writing a small deterministic fuzz target, seed-corpus + dictionary construction, sanitizer selection (ASan + UBSan default, compatibility matrix), corpus minimisation, crash-artifact handling, and CI smoke-fuzz wiring with a cached corpus. Per-engine depth (flags, harness syntax, CI jobs) lives in references, as do the corpus-management and sanitizer-integration catalogs. Use when a project needs fuzz coverage and no fuzzer is chosen yet, or when authoring / running / maintaining a fuzz campaign with any of these engines. For triaging the resulting crashes see crash-triage-reference.

70

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

libfuzzer.mdreferences/

libFuzzer (C/C++, in-process)

Per-engine reference for coverage-guided-fuzzing; the shared workflow and fuzzer-choice routing live in ../SKILL.md.

Overview

This reference wraps LLVM's libFuzzer (per llvm.org/docs/LibFuzzer.html) for C/C++ targets. Composes with:

When to use

  • Fuzzing a C / C++ library function (parser, decoder, validator).
  • Targeting a specific function - in-process fuzzing is faster than out-of-process AFL.
  • Pairing with ASan + UBSan for memory-safety + UB detection.

Authoring

The fuzz target

Define the entry point LLVMFuzzerTestOneInput:

#include <cstddef>
#include <cstdint>
#include "your_library.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    your_parser(Data, Size);
    return 0;
}

Per LLVM docs, the function signature is fixed: takes a const byte buffer + size, returns int (must return 0 for normal execution; non-zero values are reserved).

The fuzzer calls this function repeatedly with mutated Data. The target's job is to drive the library code under test and let sanitisers + asserts catch bugs.

Initialisation

Optional one-time setup:

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
    your_library_init();
    return 0;
}

Build

Standard build flag:

clang -g -O1 \
  -fsanitize=fuzzer,address,undefined \
  -fno-sanitize-recover=all \
  -fno-omit-frame-pointer \
  fuzz_target.cc your_library.cc -o fuzz_target

Per sanitizer-integration.md: ASan + UBSan is the default pair; add MSan in a separate binary if needed.

Tips for an effective target

TipWhy
Keep the target smallFaster iteration; clearer coverage
Avoid global state between runsCross-input contamination defeats coverage guidance
Use the full inputDon't if (Size < 100) return 0; unless the lib requires
Avoid expensive I/O / network in the targetSlows iterations
Use FuzzedDataProvider for structured inputsSplits Data into typed sub-values

FuzzedDataProvider (from LLVM's compiler-rt/include/fuzzer/FuzzedDataProvider.h):

#include <fuzzer/FuzzedDataProvider.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    FuzzedDataProvider fdp(Data, Size);
    int port = fdp.ConsumeIntegralInRange(1, 65535);
    std::string host = fdp.ConsumeRandomLengthString(64);
    std::vector<uint8_t> body = fdp.ConsumeRemainingBytes<uint8_t>();
    your_request_handler(host, port, body);
    return 0;
}

Running

Basic run

mkdir corpus/ seeds/
# Populate seeds/ with hand-curated inputs
./fuzz_target -max_total_time=3600 corpus/ seeds/

The first directory is writable (evolved corpus); subsequent are read-only seeds (per corpus-management.md).

Common flags

Most-used: -max_total_time=N, -runs=N (-1 = infinite), -dict=path, -fork=N, -workers=N, -merge=1 (corpus minimisation), -rss_limit_mb=N (default 2048), -timeout=N (per-input, default 1200). Full table: see "Full flag table" below (per llvm.org/docs/LibFuzzer.html).

Parallel fuzzing

./fuzz_target -fork=8 -max_total_time=3600 corpus/ seeds/

-fork=N spawns N processes, each with its own corpus subset. Combine corpora periodically with -merge=1.

Reproducing a crash

./fuzz_target crash-<sha1>
# Sanitiser report prints to stderr; same as the original crash

Verify: confirm the replay prints the same sanitiser bug class and top stack frame as the original finding before minimising. If it does not reproduce, the artefact is stale against the current build (target or library rebuilt) - rebuild the target and re-run before proceeding.

Minimise the crash input:

./fuzz_target -minimize_crash=1 -runs=10000 crash-<sha1>
# Writes minimized-from-crash-<sha1> with the smallest reproducer

Dictionary file

For structured formats:

# fuzz.dict
"{"
"}"
"["
"]"
"true"
"false"
"null"
"\":\""

Invoke: ./fuzz_target -dict=fuzz.dict corpus/.

Parsing results

libFuzzer crash artefacts are saved as:

  • crash-<sha1> - segfault / sanitiser-detected error
  • leak-<sha1> - memory leak detected by LSan
  • timeout-<sha1> - exceeded -timeout
  • oom-<sha1> - RSS exceeded -rss_limit_mb

Each file's contents are the input bytes that triggered the crash. Pair with the sanitiser report (stderr) for stack + allocation site.

For automated parsing (e.g., file as a bug), feed the sanitiser-report output to the from-CI-failure workflow in bug-report-template (qa-bug-repro plugin):

./fuzz_target crash-<sha1> 2> sanitiser-report.txt
python scripts/file-bug-from-asan.py sanitiser-report.txt crash-<sha1>

CI integration

Short smoke fuzz (5 min) on every PR: build with -fsanitize=fuzzer,address,undefined, cache fuzz/corpus, run ./fuzz_target -max_total_time=300 fuzz/corpus fuzz/seeds, and upload crash-* / leak-* / timeout-* / oom-* artifacts. Full workflow: see "Full CI job" below.

For long-running campaigns, OSS-Fuzz (google.github.io/oss-fuzz) is the canonical infrastructure.

Anti-patterns

Anti-patternWhy it failsFix
LLVMFuzzerTestOneInput with global state mutationCross-input contamination breaks coverage signalReset state per call or use LLVMFuzzerInitialize
Fuzz target without ASan + UBSanCatches only crashes; 80%+ of bugs missedAlways compose with sanitisers
No corpus minimisation everCorpus grows unbounded; cycle time degradesWeekly -merge=1
Crash committed without minimisationLarge bug-report attachmentsAlways -minimize_crash=1
Single huge fuzz targetSlow iterations; coverage attribution opaqueSplit into multiple targets per function
Ignoring -rss_limit_mb OOMsFalse crash classSet limit explicit; or disable allocator-related target paths
No dictionary for structured formatsFuzzer slowly rediscovers grammarAlways supply -dict= for JSON / XML / SQL / proto

Limitations

  • In-process only. Doesn't fuzz inter-process boundaries; for network protocols see AFL++ in -Q (QEMU) mode or specialised tools.
  • C / C++ + Rust + Swift only. Other languages have their own fuzzers (Atheris, Jazzer, Go native).
  • Coverage instrumentation overhead. Hot inner loops slow significantly under -fsanitize=fuzzer.
  • Crash uniqueness heuristic. libFuzzer dedup is sha1-based on the input; the same bug from two inputs creates two artefacts - pair with crash-stack-deduplication tooling.
  • No coverage report by default. Use -coverage flag or external llvm-profdata + llvm-cov for line-level coverage.

References

Full flag table

Per llvm.org/docs/LibFuzzer.html:

FlagEffect
-max_total_time=NStop after N seconds
-runs=NStop after N executions (-1 = infinite)
-dict=pathUse dictionary file
-seed=NRandom seed
-fork=NRun N parallel fork-mode workers
-workers=NNumber of parallel worker processes
-jobs=NTotal number of jobs to run across workers
-merge=1Corpus minimisation mode
-print_final_stats=1Print stats summary on exit
-rss_limit_mb=NRSS memory limit (default 2048)
-timeout=NPer-input timeout in seconds (default 1200)
-only_ascii=1Restrict to ASCII bytes

Full CI job

Short smoke fuzz on every PR:

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install clang
        run: sudo apt-get install -y clang lld
      - name: Build fuzz target
        run: |
          clang++ -g -O1 \
            -fsanitize=fuzzer,address,undefined \
            -fno-sanitize-recover=all \
            -fno-omit-frame-pointer \
            fuzz/fuzz_target.cc lib/parser.cc -o fuzz_target
      - uses: actions/cache@v4
        with:
          path: fuzz/corpus
          key: fuzz-corpus-${{ github.sha }}
          restore-keys: fuzz-corpus-
      - name: Smoke fuzz (5 min)
        run: ./fuzz_target -max_total_time=300 fuzz/corpus fuzz/seeds
      - name: Upload crashes
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: crashes
          path: |
            crash-*
            leak-*
            timeout-*
            oom-*

SKILL.md

tile.json