LibFuzzer Guide: In-Process Coverage-Guided Fuzzing for C and C++
LibFuzzer is LLVM's built-in coverage-guided fuzzer for C and C++. Unlike AFL++, which runs as a separate process, LibFuzzer runs inside your program's process — making it 10-20x faster. It's integrated directly into Clang, requires no external tools, and is used by Google's OSS-Fuzz project to continuously fuzz thousands of open-source projects. Writing a LibFuzzer target takes about 10 lines of code.
LibFuzzer vs AFL++
| Aspect | LibFuzzer | AFL++ |
|---|---|---|
| Architecture | In-process | External process |
| Speed | 10,000–1M executions/sec | 1,000–5,000 executions/sec |
| Setup | Compile-time only | Requires wrapper binary |
| Source required | Yes | No (QEMU mode) |
| Corpus sharing | Manual | Automatic (parallel mode) |
| OSS-Fuzz support | Native | Yes |
| Supported languages | C, C++ | C, C++, and more |
Use LibFuzzer when:
- You're fuzzing a library (not a standalone binary)
- Maximum speed matters
- You want OSS-Fuzz integration
- You're already using Clang
Use AFL++ when:
- You need to fuzz a binary without source
- You want parallel fuzzing with automatic corpus sync
- You're fuzzing a program that reads from files or stdin
Writing a Fuzz Target
A LibFuzzer target is a C function:
// fuzz_target.c
#include <stdint.h>
#include <stddef.h>
#include "my_library.h"
// Entry point: LibFuzzer calls this repeatedly with different inputs
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// Create a null-terminated copy if needed
char *buf = malloc(size + 1);
if (!buf) return 0;
memcpy(buf, data, size);
buf[size] = '\0';
// Call the function under test
my_parse_function(buf, size);
free(buf);
return 0; // Return 0 to tell LibFuzzer the input was processed
}Rules for the fuzz target:
- Must accept
(const uint8_t *data, size_t size)parameters - Must return
0(other values reserved for future use) - Must be deterministic — same input should always produce same behavior
- Should not call
exit(),abort(), or_exit() - Global state should be reset at the start of each call (or initialized once)
C++ target:
// fuzz_json.cpp
#include <cstdint>
#include <cstddef>
#include <string>
#include "json_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
std::string input(reinterpret_cast<const char*>(data), size);
try {
auto result = JsonParser::parse(input);
// Additional invariant checks
if (result.is_object()) {
// Verify re-serialization is valid
auto reserialized = result.dump();
JsonParser::parse(reserialized); // Should not throw
}
} catch (const JsonParseError&) {
// Expected for invalid JSON
}
// Any other exception = bug
return 0;
}Building and Running
LibFuzzer is part of Clang — no separate installation needed.
Compile:
# With AddressSanitizer (highly recommended)
clang -O1 -g \
-fsanitize=fuzzer,address,undefined \
fuzz_target.c my_library.c -o fuzz_target
# Without ASAN (faster, but misses memory errors)
clang -O2 -g \
-fsanitize=fuzzer \
fuzz_target.c my_library.c -o fuzz_targetThe -fsanitize=fuzzer flag links LibFuzzer and provides the main() function — you don't write one.
Run:
# Run with a corpus directory
mkdir corpus/
echo "valid input" > corpus/seed1.txt
./fuzz_target corpus/
# Run for a fixed time
./fuzz_target corpus/ -max_total_time=60 # 60 seconds
# Run with specific seed (reproduce a crash)
./fuzz_target crash-input.binOutput:
INFO: Seed: 2374086805
INFO: Loaded 1 modules (1234 inline 8-bit counters): 1234 [0x...]
INFO: Loaded 1 PC tables (1234 PCs): 1234 [0x...]
INFO: 1 files found in corpus/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 56 ft: 57 corp: 1/1b exec/s: 2 rss: 30Mb
#4 NEW cov: 57 ft: 58 corp: 2/2b lim: 4 exec/s: 4 rss: 31Mb L: 1/1 MS: 2 ChangeByte-EraseBytes-
#512 pulse cov: 71 ft: 82 corp: 12/32b lim: 8 exec/s: 512 rss: 34MbKey metrics:
- cov: code coverage (edges hit)
- ft: features (coverage + value profile data)
- corp: corpus size (files/bytes)
- exec/s: executions per second
Corpus Management
Merge and deduplicate corpus:
# Merge corpus from multiple runs, deduplicate
./fuzz_target -merge=1 corpus_merged/ corpus1/ corpus2/Minimize corpus:
# Keep only minimal set that covers the same paths
./fuzz_target -merge=1 corpus_min/ corpus_all/Minimize a crash input:
./fuzz_target -minimize_crash=1 -max_total_time=60 crash-input.binThe minimized input reproduces the same crash with the smallest possible byte sequence — easier to understand and debug.
Sanitizers
Always compile with sanitizers. They convert undefined behavior into detectable crashes.
AddressSanitizer (ASan) — memory errors:
clang -fsanitize=fuzzer,address -O1 -g fuzz.c -o fuzzFinds: buffer overflows, use-after-free, heap/stack corruption
UndefinedBehaviorSanitizer (UBSan) — undefined behavior:
clang -fsanitize=fuzzer,undefined -O1 -g fuzz.c -o fuzzFinds: integer overflow, null pointer dereference, misaligned access
MemorySanitizer (MSan) — use of uninitialized memory:
clang -fsanitize=fuzzer,memory -O1 -g fuzz.c -o fuzzNote: MSan requires all code (including stdlib) to be instrumented, which is complex. ASan + UBSan covers most cases.
Combine sanitizers:
clang -fsanitize=fuzzer,address,undefined -O1 -g fuzz.c -o fuzzCustom Mutator
LibFuzzer's default mutations (bit flips, byte insertions, interesting values) work well for generic inputs. For structured inputs (protobuf, ASN.1, complex formats), a custom mutator generates valid-but-mutated structures:
// custom_mutator.cpp
// Called by LibFuzzer when it wants to mutate an input
extern "C" size_t LLVMFuzzerCustomMutator(
uint8_t *data, size_t size, size_t max_size, unsigned int seed) {
// Deserialize the input as a protobuf message
MyProto msg;
if (!msg.ParseFromArray(data, size)) {
// If not valid proto, return a valid one
msg.set_field("default");
}
// Apply structured mutations
std::mt19937 rng(seed);
switch (rng() % 3) {
case 0: msg.set_field(std::string(rng() % 100, 'A')); break;
case 1: msg.clear_field(); break;
case 2: msg.set_count(rng()); break;
}
// Serialize back
std::string serialized;
msg.SerializeToString(&serialized);
if (serialized.size() > max_size) return 0;
memcpy(data, serialized.data(), serialized.size());
return serialized.size();
}This technique (structure-aware fuzzing) dramatically increases the depth of fuzzing for complex input formats.
Coverage Analysis
View which code paths LibFuzzer covered:
# Compile with coverage instrumentation
clang -fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping \
-O1 -g fuzz.c my_library.c -o fuzz_cov
# Run corpus through coverage-instrumented binary
LLVM_PROFILE_FILE="fuzz.profraw" ./fuzz_cov corpus/*
# Generate coverage report
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
llvm-cov show ./fuzz_cov -instr-profile=fuzz.profdata -format=html > coverage.htmlThe HTML report shows line-by-line coverage — useful for identifying which branches the fuzzer hasn't reached yet. Manually add seed inputs that exercise uncovered branches to accelerate exploration.
Dictionary-Guided Fuzzing
A dictionary of interesting tokens helps LibFuzzer generate meaningful mutations:
# sql.dict
"SELECT"
"INSERT INTO"
"DROP TABLE"
"' OR '1'='1"
"1; DROP TABLE users--"
"UNION SELECT"./fuzz_target corpus/ -dict=sql.dictLibFuzzer splices dictionary tokens into existing corpus inputs, generating inputs that are more likely to reach SQL parsing code.
Reproducing and Triaging Crashes
When LibFuzzer finds a crash:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000050
READ of size 4 at 0x602000000050 thread T0
#0 0x55555555d2a3 in parse_header /src/parser.c:142:3
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/parser.c:142 in parse_header
artifact_prefix='./'; Test unit written to ./crash-abc123def456
Base64: aW52YWxpZA== # base64-encoded crash inputReproduce:
# The crash input is saved automatically
./fuzz_target crash-abc123def456
# Decode base64 if needed
echo "aW52YWxpZA==" | base64 -d > crash.bin
./fuzz_target crash.binMinimize:
./fuzz_target -minimize_crash=1 -max_total_time=30 crash-abc123def456
# Outputs: minimized-from-crash-abc123def456Debug with lldb/gdb:
lldb -- ./fuzz_target_no_sanitizer crash-abc123def456
run
bt # Backtrace at crash pointIntegration with OSS-Fuzz
If your project is open source, OSS-Fuzz runs your LibFuzzer targets continuously on Google's infrastructure for free.
Project structure:
oss-fuzz/projects/my-project/
build.sh # How to build the fuzz targets
Dockerfile # Container with dependencies
project.yaml # Project metadatabuild.sh:
#!/bin/bash
set -ex
# Build the library
cd my-project
./configure --enable-static
make -j$(nproc)
# Build fuzz targets
for fuzzer in fuzz_parser fuzz_decompressor fuzz_crypto; do
$CC $CFLAGS -I. $SRC/${fuzzer}.c \
-o $OUT/${fuzzer} \
$LIB_FUZZING_ENGINE ./libmyproject.a
doneOSS-Fuzz:
- Runs your fuzzers continuously
- Reports crashes to you via GitHub issues
- Verifies fixes by running the crash input after your fix
- Shows coverage reports
- Has found 10,000+ bugs in projects like Chrome, OpenSSL, curl, and FFmpeg
LibFuzzer in CI
# .github/workflows/fuzz.yml
name: Fuzz Testing
on:
push:
branches: [main]
schedule:
- cron: '0 4 * * *' # Daily at 4am
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Clang
run: |
sudo apt-get install -y clang
- name: Build fuzz targets
run: |
clang -fsanitize=fuzzer,address,undefined \
-O1 -g \
fuzz/fuzz_parser.c src/parser.c \
-I include/ \
-o fuzz_parser
- name: Run fuzz tests (corpus regression)
run: |
# Run with saved corpus only (fast, for every PR)
./fuzz_parser fuzz/corpus/parser/ -max_total_time=30
- name: Extended fuzz (scheduled only)
if: github.event_name == 'schedule'
run: |
./fuzz_parser fuzz/corpus/parser/ -max_total_time=3600
- name: Save new corpus entries
if: always()
uses: actions/upload-artifact@v4
with:
name: fuzz-corpus
path: fuzz/corpus/The corpus grows over time as LibFuzzer discovers new paths. Commit the corpus to your repository so findings aren't lost between runs.