본문으로 건너뛰기 C++ Fuzz Testing | Finding Crashes with Unexpected Input

C++ Fuzz Testing | Finding Crashes with Unexpected Input

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

AspectlibFuzzerAFL/AFL++
Modein-process (single process)out-of-process (fork)
Speedvery fast (no forking)relatively slow
CoverageLLVM SanitizerCoveragebinary instrumentation
CI fiteffective in short burstsrecommended for long runs
PlatformClang onlyGCC/Clang

2. Using libFuzzer

LLVMFuzzerTestOneInput

  • The target: define extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size). The fuzzer generates data/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

OptionDescriptionExample
-runs=Nlimit the number of runs-runs=100000
-max_total_time=Ntotal run time (seconds)-max_total_time=60
-timeout=Nper-input timeout (seconds)-timeout=1
-rss_limit_mb=Nmemory limit (MB)-rss_limit_mb=2048
-dict=filea token dictionary file-dict=keywords.dict
-artifact_prefix=pathwhere to save crash/timeout artifacts-artifact_prefix=crash_
-minimize_crash=1minimize a crashing input./fuzz < crash
-print_final_stats=1print stats on exitcoverage, 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

TopicSummary
Fuzz testingrepeatedly call a target with random/mutated input → find crash- or error-triggering inputs
libFuzzerLLVMFuzzerTestOneInput + -fsanitize=fuzzer, paired with ASan/UBSan
Complete examplesan integer parser, a URL parser, a packet parser, FuzzedDataProvider
Common errorslink errors, slow fuzzing, OOM, timeouts, insufficient seeds
CI integrationGitHub Actions, GitLab CI, 60-second fuzzing + corpus regression
Production patternsseed management, regression scripts, long-running fuzzing, crash minimization
Corpus & CIstore 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]
  1. Identify the target: parser/decoder/protocol-handling functions that receive external input
  2. Write a wrapper: an LLVMFuzzerTestOneInput that forwards data/size to the target
  3. Build: with -fsanitize=fuzzer,address,undefined
  4. Test locally: run for 1 minute and check for crashes/errors
  5. Fix bugs: once found, fix them and save the crashing input to the corpus
  6. Add seeds: put valid-format samples into corpus/
  7. Integrate with CI: fuzz for 60+ seconds on every push
  8. Manage the corpus: version-control crashing inputs with git
  9. 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

Other articles related to this topic.

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