C++ Fuzz Testing | Finding Crashes with Unexpected Input
Introduction: “I never imagined an input like this”
Unexpected input is what opens up bugs
If article 41-1 covered static analysis and 41-2 covered runtime sanitizers to catch known patterns and memory/race issues at runtime, fuzz testing automatically finds “this input breaks it” by continuously feeding random, mutated input into your program. It’s especially effective for code that accepts external input — parsers, decoders, protocol handlers. libFuzzer (from LLVM) is an in-process, coverage-guided fuzzer: you define a single fuzz target function, and it repeatedly calls that function with byte arrays. Combined with ASan and UBSan, it can also catch things like overflows and integer overflow. What this article covers
- The concept of fuzz testing: a fuzzer generates and mutates input to repeatedly call a target
- libFuzzer: LLVMFuzzerTestOneInput, build options, corpora
- Complete fuzzing examples: a JSON parser, a URL parser, protocol handling
- Common errors and how to fix them
- CI integration: GitHub Actions, GitLab CI
- Production patterns: seed management, regression tests, long-running fuzzing campaigns
The problem scenario
Scenario 1: a JSON parser crash Your production server’s JSON API crashes occasionally. You can’t reproduce it.
// ❌ Problem: crashes when external JSON input takes an unexpected shape
// e.g. {"key": 123456789012345678901234567890} // integer overflow
// e.g. {"key": "AAAAAAAA..."} // very long string → buffer overrun
// e.g. {"key": [[[[[[[[[[... // deep nesting → stack overflow
void handle_request(const std::string& json) {
auto parser = JSONParser();
auto result = parser.parse(json); // crashes here!
process(result);
}
Scenario 2: a URL parser vulnerability Code that parses user-supplied URLs crashes on certain URL formats.
// ❌ Problem: missing bounds checks while parsing the URL
// e.g. "http://" + 10000 'A' characters + "://"
// e.g. "file://" + ".." * 1000
// e.g. "%" + an arbitrary byte
struct ParsedURL {
std::string scheme;
std::string host;
int port;
};
ParsedURL parse_url(const char* url, size_t len); // no check on len?
Scenario 3: binary protocol handling A binary protocol parser suffers a memory overrun when it receives a packet with a forged length field.
// ❌ Problem: trusting the packet's length field
// e.g. length = 0xFFFFFFFF, actual payload = 4 bytes
// type definition
struct Packet {
uint32_t length; // attacker-controllable
uint8_t data[]; // reading `length` bytes overruns the buffer!
};
void process_packet(const uint8_t* buf, size_t size) {
if (size < 4) return;
uint32_t len = read_u32(buf);
memcpy(dest, buf + 4, len); // dangerous if len > size-4!
}
Why does this happen? Developers think about and test only normal input. But attackers or buggy clients send malformed data, extreme values, and edge cases. Manual testing can’t cover all of these. Fuzz testing automatically fills that gap.
1. The Concept of Fuzz Testing
Automated injection of malformed input
- A fuzzer repeatedly calls a target function with input bytes (or structured data). Inputs are generated randomly or by mutating an existing corpus, exploring more paths based on “inputs that previously triggered a crash.”
- Goal: find inputs that trigger a crash, an assertion failure, or a sanitizer detection. Once found, the input is saved to the corpus for use as a regression test.
- Good candidates: code that interprets a byte stream — parsing, decoding, serialization/deserialization, file-format handling, network protocol handling.
The fuzz testing loop
flowchart TB
subgraph Input[Input generation]
A[Random bytes]
B[Corpus seed]
C[Mutation]
end
subgraph Fuzz[Fuzz loop]
D[Call target function]
E{Crash/error?}
F[Save to corpus]
G[New path found?]
end
subgraph Output[Output]
H[Save the crashing input]
I[For regression testing]
end
A --> D
B --> C --> D
D --> E
E -->|Yes| F --> H
E -->|No| G -->|Yes| F
G -->|No| D
H --> I
libFuzzer vs. AFL
| Aspect | libFuzzer | AFL/AFL++ |
|---|---|---|
| Mode | in-process (single process) | out-of-process (fork) |
| Speed | very fast (no forking) | relatively slow |
| Coverage | LLVM SanitizerCoverage | binary instrumentation |
| CI fit | effective in short bursts | recommended for long runs |
| Platform | Clang only | GCC/Clang |
2. Using libFuzzer
LLVMFuzzerTestOneInput
- The target: define
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size). The fuzzer generatesdata/size(randomly or via corpus mutation) and calls this function repeatedly. Call your parser or decoder from inside it. The fuzzer passes arbitrary (or corpus-derived, mutated) bytes as data and size, so inside this function you call your actual parser/decoder — for example, p.parse(data, size). If a crash, assertion, or ASan detection occurs here, libFuzzer saves that input and stops. A minimum-length check like size < 4 lets you quickly skip meaningless short inputs. The return value is normally 0, and the fuzzer calls this function an unbounded number of times.
#include <stddef.h>
#include <stdint.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 4) return 0;
MyParser p;
p.parse(data, size); // the fuzzer saves the input on a crash or assertion
return 0;
}
Build options
# Build with libFuzzer + ASan + UBSan using Clang
clang++ -std=c++17 -g -fsanitize=fuzzer,address,undefined \
-fno-omit-frame-pointer \
-o fuzz_target fuzz_target.cpp my_parser.cpp
# CMake example
add_executable(fuzz_target fuzz_target.cpp my_parser.cpp)
target_compile_options(fuzz_target PRIVATE
-fsanitize=fuzzer,address,undefined
-fno-omit-frame-pointer
)
target_link_options(fuzz_target PRIVATE
-fsanitize=fuzzer,address,undefined
)
Running it
# Run indefinitely (Ctrl+C to stop)
./fuzz_target
# Run exactly 10,000 iterations
./fuzz_target -runs=10000
# Use a corpus directory (seeds + newly discovered inputs are saved)
mkdir -p corpus
./fuzz_target corpus
# Set a per-input timeout (1 second)
./fuzz_target -timeout=1 corpus
# Minimize a crashing input
./fuzz_target -minimize_crash=1 < crash_input
Key libFuzzer options
| Option | Description | Example |
|---|---|---|
| -runs=N | limit the number of runs | -runs=100000 |
| -max_total_time=N | total run time (seconds) | -max_total_time=60 |
| -timeout=N | per-input timeout (seconds) | -timeout=1 |
| -rss_limit_mb=N | memory limit (MB) | -rss_limit_mb=2048 |
| -dict=file | a token dictionary file | -dict=keywords.dict |
| -artifact_prefix=path | where to save crash/timeout artifacts | -artifact_prefix=crash_ |
| -minimize_crash=1 | minimize a crashing input | ./fuzz < crash |
| -print_final_stats=1 | print stats on exit | coverage, run count, etc. |
3. Complete Fuzz Testing Examples
Example 1: a simple integer parser
Fuzzing code that parses a length-prefixed array of integers.
// simple_int_parser.hpp
#pragma once
#include <cstdint>
#include <vector>
#include <stdexcept>
// A dangerous parser: insufficient bounds checking
class SimpleIntParser {
public:
std::vector<int32_t> parse(const uint8_t* data, size_t size) {
std::vector<int32_t> result;
if (size < 4) return result;
uint32_t count = (data[0] << 24) | (data[1] << 16) |
(data[2] << 8) | data[3];
// ❌ No validation of count! Must be compared against size
size_t needed = 4 + count * 4;
if (size < needed) return result; // ✅ Fix: bounds check
for (uint32_t i = 0; i < count; ++i) {
size_t offset = 4 + i * 4;
int32_t val = (data[offset] << 24) | (data[offset+1] << 16) |
(data[offset+2] << 8) | data[offset+3];
result.push_back(val);
}
return result;
}
};
// fuzz_simple_parser.cpp
#include <stddef.h>
#include <stdint.h>
#include "simple_int_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 4) return 0; // minimum: a 4-byte length
SimpleIntParser parser;
(void)parser.parse(data, size);
return 0;
}
# Build and run
clang++ -std=c++17 -g -fsanitize=fuzzer,address,undefined \
-o fuzz_simple fuzz_simple_parser.cpp simple_int_parser.cpp
./fuzz_simple -runs=100000 corpus
Example 2: fuzzing a URL parser
// url_parser.hpp
#pragma once
#include <string>
#include <cstdint>
struct ParsedURL {
std::string scheme;
std::string host;
std::string path;
uint16_t port = 0;
bool valid = false;
};
ParsedURL parse_url(const uint8_t* data, size_t size);
// url_parser.cpp - the fuzz target
#include "url_parser.hpp"
#include <cstring>
#include <algorithm>
ParsedURL parse_url(const uint8_t* data, size_t size) {
ParsedURL out;
if (size == 0) return out;
const char* p = reinterpret_cast<const char*>(data);
const char* end = p + size;
// extract the scheme (e.g. "http:")
const char* colon = static_cast<const char*>(std::memchr(p, ':', size));
if (!colon || colon >= end - 1) return out;
out.scheme.assign(p, colon - p);
p = colon + 1;
// skip "//"
if (end - p >= 2 && p[0] == '/' && p[1] == '/') p += 2;
// host (up to the next '/' or the end)
const char* slash = static_cast<const char*>(std::memchr(p, '/', end - p));
if (slash) {
out.host.assign(p, slash - p);
out.path.assign(slash, end - slash);
} else {
out.host.assign(p, end - p);
}
out.valid = true;
return out;
}
// fuzz_url_parser.cpp
#include <stddef.h>
#include <stdint.h>
#include "url_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size > 65536) return 0; // cap very large inputs
ParsedURL result = parse_url(data, size);
(void)result;
return 0;
}
Example 3: a protocol packet parser (length-field validation)
// packet_parser.hpp
#pragma once
#include <cstdint>
#include <vector>
struct Packet {
uint32_t type;
std::vector<uint8_t> payload;
};
// A safe parser: validates the length field
bool parse_packet(const uint8_t* data, size_t size, Packet& out);
// packet_parser.cpp
#include "packet_parser.hpp"
#include <cstring>
bool parse_packet(const uint8_t* data, size_t size, Packet& out) {
if (size < 8) return false; // type(4) + length(4)
out.type = (data[0] << 24) | (data[1] << 16) |
(data[2] << 8) | data[3];
uint32_t len = (data[4] << 24) | (data[5] << 16) |
(data[6] << 8) | data[7];
// ✅ Key point: validate the length - safe even if an attacker forges len
if (len > size - 8) return false;
if (len > 1024 * 1024) return false; // cap at 1MB
out.payload.assign(data + 8, data + 8 + len);
return true;
}
// fuzz_packet_parser.cpp
#include <stddef.h>
#include <stdint.h>
#include "packet_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
Packet pkt;
(void)parse_packet(data, size, pkt);
return 0;
}
Example 4: structured input (FuzzedDataProvider)
libFuzzer’s FuzzedDataProvider makes it easy to break a byte stream into integers, strings, vectors, and more. Header path: it ships with compiler-rt when you install Clang; use it via #include <fuzzer/FuzzedDataProvider.h>.
// fuzz_with_provider.cpp
#include <stddef.h>
#include <stdint.h>
#include <fuzzer/FuzzedDataProvider.h>
#include "my_api.hpp"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 4) return 0;
FuzzedDataProvider provider(data, size);
// build structured input
int mode = provider.ConsumeIntegralInRange<int>(0, 3);
std::string str = provider.ConsumeRandomLengthString(256);
std::vector<uint8_t> bytes = provider.ConsumeBytes<uint8_t>(
provider.ConsumeIntegralInRange<size_t>(0, 1024));
my_api_process(mode, str, bytes);
return 0;
}
# FuzzedDataProvider ships with compiler-rt (bundled with Clang)
clang++ -std=c++17 -g -fsanitize=fuzzer,address \
-I$(dirname $(clang++ -print-file-name=libclang_rt.fuzzer.a))/../include \
-o fuzz_provider fuzz_with_provider.cpp
4. Common Errors and Fixes
Issue 1: “undefined reference to LLVMFuzzerTestOneInput”
Cause: -fsanitize=fuzzer was omitted at link time, or the function signature is wrong.
Fix:
// ✅ The correct signature (extern "C" is required)
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// ...
return 0;
}
# ✅ Make sure the fuzzer flag is included when linking
clang++ -fsanitize=fuzzer,address -o fuzz_target fuzz_target.cpp
Issue 2: fuzzing is far too slow (fewer than 10 execs/sec)
Cause: a heavy target function, I/O, or large allocations. Fixes
- Filter tiny inputs early:
if (size < N) return 0;to skip short inputs - Cap input size:
if (size > 64*1024) return 0; - Timeout:
-timeout=1(1 second per input) - Remove I/O: use an in-memory buffer instead of files/network
// ❌ Slow: writes a file on every call
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
std::ofstream f("/tmp/input");
f.write(reinterpret_cast<const char*>(data), size);
return parse_file("/tmp/input"); // I/O bottleneck!
}
// ✅ Fast: operate directly on memory
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
return parse_from_memory(data, size);
}
Issue 3: the process dies from OOM (Out of Memory)
Cause: a particular input triggers a huge allocation (e.g. vector<int>(count) where count=0xFFFFFFFF).
Fixes
- Validate length/size upper bounds
- Cap memory with -rss_limit_mb=N
// ✅ Validate an upper bound inside the parser
if (count > 1024 * 1024) return {}; // reject more than 1M elements
# Cap memory at 2GB
./fuzz_target -rss_limit_mb=2048 corpus
Issue 4: infinite loops / timeouts
Cause: a particular input drives the parser into an infinite loop (e.g. regex ReDoS, deep recursion). Fixes
- -timeout=1: cap each input at 1 second
- Add a recursion-depth limit to the parser
- -ignore_timeouts=0: also save timing-out inputs (ignored by default)
./fuzz_target -timeout=1 -ignore_timeouts=0 corpus
Issue 5: ASan and libFuzzer conflicts
Cause: in some environments, combining -fsanitize=fuzzer,address causes a link error.
Fixes
- Use a recent Clang version (10+)
- Check the ordering when specifying -fsanitize=fuzzer together with -fsanitize=address
# ✅ A combination that usually works
clang++ -fsanitize=address,undefined,fuzzer -o fuzz fuzz.cpp
Issue 6: the corpus is empty or ineffective
Cause: with no seeds, the fuzzer relies purely on random mutation, which struggles to explore deep paths for a structured format. Fixes
- Add meaningful seeds: put valid JSON, URL, or packet samples in
corpus/ - Use -dict=file: supply a dictionary of keywords/tokens
# url.dict - for the URL parser
http
https
ftp
://
/
?
#
%
./fuzz_target -dict=url.dict corpus
Issue 7: can’t find the FuzzedDataProvider header
Cause: the path to FuzzedDataProvider.h varies by Clang version.
Fix:
# Find the header path (Clang 14+)
clang++ -print-resource-dir
# Output: /usr/lib/llvm-14/lib/clang/14.0.0
# Header: /usr/lib/llvm-14/lib/clang/14.0.0/include/fuzzer/FuzzedDataProvider.h
# Add the include path in CMake
target_include_directories(fuzz_target PRIVATE
${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}/include
)
Alternatively, you can copy FuzzedDataProvider.h directly into your project.
Issue 8: LeakSanitizer reports memory leaks during fuzzing
Cause: the target code allocates memory it doesn’t explicitly free. Since the fuzzer calls it repeatedly, this accumulates into an OOM. Fixes
- -detect_leaks=0: disable leak detection while fuzzing (focus on finding crashes)
- Or explicitly free resources allocated by the target code
# Disable leak detection while fuzzing (keeps ASan active)
LSAN_OPTIONS=detect_leaks=0 ./fuzz_target corpus
5. CI Integration
GitHub Actions
# .github/workflows/fuzz.yml
name: Fuzz Testing
on:
push:
branches: [main, develop]
schedule:
- cron: '0 2 * * *' # a long fuzzing run every night at 2 AM
jobs:
fuzz-quick:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Clang
run: |
sudo apt-get update
sudo apt-get install -y clang-15
- name: Build fuzz target
run: |
export CC=clang-15
export CXX=clang++-15
mkdir build && cd build
cmake ...-DCMAKE_CXX_FLAGS="-fsanitize=fuzzer,address,undefined"
cmake --build . --target fuzz_target
- name: Run fuzz (60 seconds)
run: |
cd build
mkdir -p corpus
# reuse the existing corpus if present
if [ -d ../corpus ]; then cp -r ../corpus .; fi
timeout 60 ./fuzz_target corpus -runs=100000 || true
# on failure, the crashing input is saved into corpus
- name: Upload corpus on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-corpus-crash
path: build/corpus/
fuzz-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and run regression
run: |
# replay only the corpus - confirm no new crashes
./scripts/fuzz_regression.sh
GitLab CI
# .gitlab-ci.yml
fuzz:
stage: test
image: ubuntu:22.04
variables:
CC: clang-15
CXX: clang++-15
before_script:
- apt-get update && apt-get install -y clang-15 cmake
script:
- mkdir build && cd build
- cmake ...-DFUZZ=ON
- cmake --build .
- mkdir -p corpus
- timeout 120 ./fuzz_target corpus
artifacts:
when: on_failure
paths:
- build/corpus/
expire_in: 7 days
Configuring a CMake fuzz target
# CMakeLists.txt
option(FUZZ "Build fuzz targets" OFF)
if(FUZZ)
add_executable(fuzz_target
fuzz_target.cpp
${LIB_SOURCES}
)
target_compile_options(fuzz_target PRIVATE
-fsanitize=fuzzer,address,undefined
-fno-omit-frame-pointer
-g
)
target_link_options(fuzz_target PRIVATE
-fsanitize=fuzzer,address,undefined
)
target_include_directories(fuzz_target PRIVATE ${CMAKE_SOURCE_DIR}/src)
endif()
6. Production Patterns
Pattern 1: managing the seed corpus
# corpus/ directory structure
corpus/
├── seed_001.json # a valid JSON sample
├── seed_002.json
├── url_001.txt # various URL formats
├── packet_001.bin # a valid packet
└── crash_abc123 # a previously found crashing input (for regression)
- Seeds: the fuzzer’s starting point for mutation. More valid, well-formed seeds help it explore deeper paths.
- Crashing inputs: used as regression tests after fixing a bug. Track them with
git add corpus/.
Pattern 2: a regression test script
#!/bin/bash
# scripts/fuzz_regression.sh
set -e
BUILD_DIR=build
CORPUS=corpus
# skip if there's no corpus
if [ ! -d "$CORPUS" ]; then
echo "No corpus, skipping regression"
exit 0
fi
# build the fuzz target
cmake --build $BUILD_DIR --target fuzz_target
echo "Running regression on $(find $CORPUS -type f | wc -l) inputs"
./$BUILD_DIR/fuzz_target $CORPUS -runs=0
# -runs=0: only replay the corpus, don't generate new inputs
echo "Regression passed"
Pattern 3: long-running fuzzing (overnight/weekends)
#!/bin/bash
# fuzz for 24 hours, saving results to a separate directory
CORPUS_DIR=corpus
ARTIFACT_DIR=fuzz_artifacts_$(date +%Y%m%d)
mkdir -p $ARTIFACT_DIR
./fuzz_target $CORPUS_DIR -max_total_time=86400 \
-artifact_prefix=$ARTIFACT_DIR/ \
-print_final_stats=1
# crashes found are saved into $ARTIFACT_DIR/
Pattern 4: parallel fuzzing across multiple targets
# run multiple fuzz targets simultaneously
for target in fuzz_json fuzz_url fuzz_packet; do
./$target corpus_$target -max_total_time=3600 &
done
wait
Pattern 5: minimizing a crashing input
# confirm the fix using a minimized input
./fuzz_target -minimize_crash=1 < crash_input
# output: a minimized input (still triggers the same bug, but shorter)
Pattern 6: running AFL++ alongside libFuzzer
For formats with complex structure, AFL++‘s mutation strategy can be more effective. A common pattern is using libFuzzer in CI and AFL++ for long overnight fuzzing runs.
# Build for AFL++ (using afl-clang-fast)
export CC=afl-clang-fast
export CXX=afl-clang-fast++
afl-fuzz -i corpus -o afl_output -m none -- ./fuzz_target @@
# Convert AFL output into a libFuzzer corpus
# copy files from afl_output/default/crashes/ into corpus/
Pattern 7: OSS-Fuzz-style integration
Google’s OSS-Fuzz provides free fuzzing infrastructure for open-source projects. You can build your own fuzzing pipeline with a similar structure.
# Dockerfile.fuzz (simplified example)
FROM gcr.io/oss-fuzz-base/base-builder
RUN apt-get update && apt-get install -y clang cmake
COPY . /src
WORKDIR /src
RUN ./build_fuzz.sh
# build_fuzz.sh
mkdir build && cd build
cmake ...-DFUZZ=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake --build . --target fuzz_target
cp fuzz_target $OUT/
7. Corpus and Regression Testing
Seeds and regression testing
- Corpus: a directory of input files that have triggered crashes or discovered new paths. Managing it with git lets you check, on regression, whether “this input still triggers the bug.” Minimization (e.g. -minimize_crash=1) can shrink an input down while still triggering the same bug.
- CI: build the fuzz target with sanitizer + fuzzer flags, and either run it for a short time (e.g. 60 seconds) or just replay the existing corpus to check for regressions. Long-running fuzzing is often scheduled separately (e.g. overnight).
- Timeouts: when an input times out, the fuzzer can record it as a “slow input” and run it under a time limit going forward. Configure this with -timeout=1 and similar flags.
Corpus directory strategy
flowchart LR
subgraph Sources[Seed sources]
A[Valid samples]
B[Past crashes]
C[Manual test cases]
end
subgraph Corpus[Corpus]
D[corpus/]
end
subgraph CI[CI]
E[Build]
F[Regression: replay corpus only]
G[New: 60-second fuzzing]
end
A --> D
B --> D
C --> D
D --> E --> F
E --> G
G -->|crash found| B
8. Summary
| Topic | Summary |
|---|---|
| Fuzz testing | repeatedly call a target with random/mutated input → find crash- or error-triggering inputs |
| libFuzzer | LLVMFuzzerTestOneInput + -fsanitize=fuzzer, paired with ASan/UBSan |
| Complete examples | an integer parser, a URL parser, a packet parser, FuzzedDataProvider |
| Common errors | link errors, slow fuzzing, OOM, timeouts, insufficient seeds |
| CI integration | GitHub Actions, GitLab CI, 60-second fuzzing + corpus regression |
| Production patterns | seed management, regression scripts, long-running fuzzing, crash minimization |
| Corpus & CI | store seeds, run regression tests, short runs or corpus-only replay in CI |
| The 41-series completed a system for “blocking bugs before they happen”: static analysis (Clang-Tidy, Cppcheck) → runtime verification (ASan, TSan) → fuzz testing (libFuzzer). |
Applying This in Order
The following example demonstrates the concept in mermaid:
flowchart TD
A[1. Identify the target function] --> B[2. Write an LLVMFuzzerTestOneInput wrapper]
B --> C[3. Build with sanitizer flags]
C --> D[4. Fuzz locally for 1 minute]
D --> E{Crash?}
E -->|Yes| F[5. Fix the bug and rerun]
E -->|No| G[6. Add to the seed corpus]
F --> D
G --> H[7. Add a fuzz job to CI]
H --> I[8. Track the corpus in git]
I --> J[9. Schedule long-running fuzzing]
- Identify the target: parser/decoder/protocol-handling functions that receive external input
- Write a wrapper: an
LLVMFuzzerTestOneInputthat forwardsdata/sizeto the target - Build: with
-fsanitize=fuzzer,address,undefined - Test locally: run for 1 minute and check for crashes/errors
- Fix bugs: once found, fix them and save the crashing input to the corpus
- Add seeds: put valid-format samples into
corpus/ - Integrate with CI: fuzz for 60+ seconds on every push
- Manage the corpus: version-control crashing inputs with git
- Long-running fuzzing: run for hours to 24 hours overnight/on weekends
Implementation Checklist
- Define an LLVMFuzzerTestOneInput target function
- Build with -fsanitize=fuzzer,address,undefined
- Filter minimum/maximum input size
- Prepare a meaningful seed corpus
- Add a fuzz job to CI (60+ seconds)
- A regression test script (replay the corpus only)
- Version-control crashing inputs with git
- Configure -timeout and -rss_limit_mb
Related Articles (Internal Links)
Other articles related to this topic.
- C++ WebAssembly (Wasm) and Emscripten | Running C++ in the Browser [#35-2]
- C++ Direct Hardware Control: volatile, Memory-Mapped I/O, and Interrupt Service Routines [#42-2]
- C++ Segmentation Fault | Core Dumps
Practical tips (fuzzing)
- Start from a small, fast corpus; expand coverage before chasing exotic inputs.
- Use sanitizers (ASan/UBSan) with libFuzzer to catch memory errors close to the mutation.
- Minimize crashing inputs before filing bugs.
Checklist
- Fuzz target is deterministic when seeds are fixed?
- Time limits and artifact storage for crashes are defined?
Keywords Covered in This Article (Related Search Terms)
This article covers fuzzing, fuzz testing, C++ fuzzing, libFuzzer, AFL, robustness testing.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. If you have C++ code that interprets external input — parsers, decoders, protocol handling, file-format handling — apply fuzz testing. It automatically finds bugs caused by “unexpected input” that static and dynamic analysis alone can’t catch.
Q. What’s the difference between fuzz testing and unit testing?
A. Unit tests verify intended behavior with developer-written input. Fuzz testing looks for unintended behavior (crashes, errors) using fuzzer-generated, random/mutated input. You need both.
Q. How long should fuzzing run in CI?
A. Run it briefly — around 60-120 seconds — on every PR, and recommend hours to 24 hours of long-running fuzzing after merging to main/develop or on a nightly schedule.
Q. What should I read before this?
A. Follow the previous article link at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. See the official libFuzzer documentation, the OSS-Fuzz project, and cppreference.
Q. How do I measure how effective fuzzing has been?
A. Measure it via execution stats (executions per second, total executions), code coverage (libFuzzer’s -print_coverage=1 shows path counts), and the number of crashes found. If the corpus grows over time, you’re exploring more paths.
Q. Is it OK to include a fuzz target in a production build?
A. Not recommended. Fuzz targets are built with flags like -fsanitize=fuzzer,address, which add significant overhead, and LLVMFuzzerTestOneInput is never called from ordinary user code. Build it only as a separate test target, and run it only in CI/development environments.
One-line summary: fuzzing lets you automatically verify robustness against unexpected input. Next, consider reading Embedded C++ and No Exceptions/RTTI (#42-1).
Previous article: Hardening #41-2: ASan and TSan
Next article: [Practical Domains #42-1] C++ in Constrained Environments: Writing Safe Code Without Exceptions and RTTI
Related Articles
- Integrating C++ Static Analysis Tools: Enforcing Code Quality with Clang-Tidy and Cppcheck [#41-1]
- C++ volatile Complete Guide | MMIO, ISRs, Memory-Mapped Registers, and the Difference from atomic [Practical]
- C++ Runtime Verification: A Complete Guide to AddressSanitizer and ThreadSanitizer [#41-2]
- C++ [[nodiscard]] Complete Guide | Preventing Ignored Return Values, Error Codes, RAII, and Reason Messages [Practical]