Safe Penetration Testing Labs · advanced · ~25 min

Fuzz harness design — defensively

By the end of this lesson you will be able to: - Write a correct, deterministic **libFuzzer entry point** (`LLVMFuzzerTestOneInput`) around a single C parser or validator. - Explain the roles of the **fuzzer**, the **harness**, the **corpus**, and the **sanitizers** — and how they fit together as a bug-finding pipeline. - Turn raw fuzzer bytes into a **safe, NUL-terminated input** without introducing bugs the harness itself owns (leaks, over-reads, false crashes). - Build and run a harness with **AddressSanitizer (ASan)** and **UndefinedBehaviorSanitizer (UBSan)**, and **replay** a saved crash artifact to reproduce a finding. - Apply the **authorization and cleanup rules** for running fuzzing safely in a local, isolated lab. - Recognise when a crash is a **real defect** to report versus a bug you accidentally put in the harness.

Overview

Security objective. The asset you are protecting is the memory safety and availability of a program that parses untrusted input — a username validator, a config reader, a network-message parser. The threat is an attacker who sends malformed or hostile input that triggers a buffer overflow, out-of-bounds read, use-after-free, or undefined behaviour, leading to a crash (denial of service) or, worse, memory corruption an attacker can steer. A fuzz harness lets you find those bugs first, on your own machine, before an attacker does — so this is a purely defensive technique.

A fuzz harness is a small piece of code that hands one of your functions to a fuzzer such as libFuzzer or AFL++. The roles are deliberately narrow:

  • The fuzzer generates and mutates inputs, guided by code-coverage feedback, running the target millions of times.
  • The harness takes each input and feeds it to the one function you want to test.
  • When the target crashes, the fuzzer saves the exact input to disk as a crash artifact so you can replay and debug it later.

This lesson builds directly on your prerequisites. From Safe parsing — the defensive parser shape you already know that a parser must check lengths before it reads and reject malformed input at the boundary; fuzzing is how you prove your parser actually does that under adversarial pressure. From Memory safety — the whole bug class you know the failure modes (overflow, over-read, use-after-free, uninitialised reads); fuzzing plus sanitizers is the industrial way to surface those failures automatically instead of hoping a code review catches them. The harness you write here is the glue that connects those two ideas: it drives your defensive parser with the exact kind of hostile input the memory-safety lesson warned you about.

Why it matters

In authorized professional work — secure development, product-security review, an internal red team testing your own company's software — fuzzing is the highest-leverage way to find memory-safety bugs in parsers. A five-line harness around a single validator, run for an hour with sanitizers on, routinely finds bugs that survived years of manual review.

The reason it matters so much:

  • Parsers face untrusted input by definition. Anything that reads a file, a packet, a header, or a form field is an attack surface. These are exactly the functions worth fuzzing.
  • Sanitizers turn silent corruption into loud, reproducible crashes. Pair the harness with AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) and every latent bug becomes a clean backtrace you can act on — not a once-a-month heisenbug in production.
  • It scales into CI. Once a harness exists, you run it on every commit with a time budget. New parsing bugs get caught the day they are introduced.
  • It is how the ecosystem works. Google's OSS-Fuzz continuously fuzzes hundreds of open-source projects; curl, OpenSSL, SQLite, and nginx all ship fuzz harnesses. Being able to write one is a core secure-engineering skill, not a niche one.

Crucially, fuzzing is defensive: you run it against code you own or are authorized to test, in an isolated environment, to remediate bugs. It is not a tool for attacking someone else's live systems.

Core concepts

1. The fuzzer

Definition. A program that repeatedly generates, mutates, and feeds inputs to your target, watching for crashes and using code-coverage feedback to reach new paths.

Plain explanation. Think of it as a tireless tester that starts from some sample inputs (the corpus) and keeps tweaking bytes — flipping bits, splicing, growing, shrinking — favouring mutations that exercise new lines of code.

How it works. Coverage-guided fuzzers (libFuzzer, AFL++) instrument your code at compile time. Each run reports which edges were hit; inputs that reach new code are kept and mutated further. This is far more effective than blind random input.

When / when not. Ideal for functions that take a byte buffer and parse it. Poor fit for functions that need a complex live environment (open sockets, a populated database) unless you stub that environment out.

Pitfall. Without a seed corpus and without coverage instrumentation, fuzzing degrades to slow random testing that rarely gets past the first length check.

2. The harness (the entry point)

Definition. The single function libFuzzer calls for every input:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);

Plain explanation. It receives a pointer to size bytes the fuzzer produced, converts them into whatever shape your function expects, calls that function, cleans up, and returns 0.

How it works. You compile this function together with your parser and link against libFuzzer (-fsanitize=fuzzer). The resulting binary is the fuzzer for that target.

When / when not. One harness targets one function or one clearly-bounded feature. Do not try to fuzz your whole program through a single entry point — small targets find bugs faster and give cleaner reports.

Pitfall. Treating data as a C string. It is not NUL-terminated and may contain embedded \0 bytes. You must copy it into a size + 1 buffer and add the terminator yourself if the parser expects a string.

3. Determinism

Definition. The same input must always produce the same result.

Plain explanation. A harness must not depend on the clock, randomness, global mutable state, network, or files. If run N of the same input can crash while run N+1 does not, the bug cannot be reproduced and the finding is worthless.

Pitfall. Reading rand(), time(), an environment variable, or leftover global state from a previous input. Reset or avoid all of it.

4. Cleanup and the harness's own bugs

Definition. Free every allocation the harness makes, and never over-read or over-write in the glue code.

Plain explanation. The fuzzer cannot tell your bug from the parser's bug. A leak in the harness gets blamed on the parser; an over-read in the harness is a false crash that wastes hours.

Pitfall. Forgetting free(copy), or writing copy[size] into a buffer of only size bytes.

5. Sanitizers

Definition. Compiler instrumentation that detects memory and undefined-behaviour errors at runtime.

  • ASan (AddressSanitizer): heap/stack/global buffer overflows, use-after-free, double-free.
  • UBSan (UndefinedBehaviorSanitizer): signed overflow, invalid shifts, misaligned or null pointer use, out-of-range enum values.

Plain explanation. Without a sanitizer a bad read might return garbage and look fine; with ASan it aborts immediately with a precise report and backtrace. Sanitizers are what convert "weird behaviour" into "actionable crash."

Pitfall. Fuzzing without sanitizers finds only the bugs that already crash on their own — a small fraction of the real total.

6. Crash artifacts and replay

Definition. When the target crashes, libFuzzer writes the offending bytes to a file, by default named crash-<sha1>.

Plain explanation. That file is the reproducer. Run the same fuzz binary with the file as an argument and it replays exactly that one input, deterministically, so you can debug it under gdb or read the ASan report.

Pitfall. Deleting crash files before triaging them, or committing them to a public repo (they can encode sensitive input shapes). Store them with the finding, in the lab.

Threat model (text diagram)

TRUST BOUNDARY: your program's parser boundary

   Untrusted input                Your program (the ASSET)
   ----------------               ---------------------------
   files / packets / ==========>  parse_input()   <-- attack surface
   form fields                    |  reads header, fields
   (attacker-controlled)          |  MUST length-check first
                                   v
                                internal state / memory

   FUZZING (authorized lab, localhost only):
   ----------------------------------------
   libFuzzer  --mutates-->  LLVMFuzzerTestOneInput  --calls-->  parse_input()
        ^                          (the HARNESS)                    |
        |                                                           v
        +------ crash artifact <----- ASan/UBSan detect memory/UB error

   Entry point under test:  the parser (right of the trust boundary)
   Who supplies bytes in prod:  the attacker
   Who supplies bytes in the lab:  the fuzzer (standing in for the attacker)

Knowledge check.

  1. What asset is protected by fuzzing a parser, and where exactly is the trust boundary in the diagram?
  2. The classic insecure assumption behind a parser crash is "the input is at least as long as the header I'm about to read." How does a fuzzer expose that assumption, and which prerequisite lesson taught you to guard it?
  3. Why must this fuzzing only be run in a local, isolated, authorized lab rather than against a running production service or someone else's host?

Syntax notes

The one function libFuzzer requires. In C you do not need extern "C" (that is only for C++ harnesses, to prevent name mangling):

#include <stdint.h>   /* uint8_t */
#include <stddef.h>   /* size_t  */

/* Called once per fuzzer-generated input.
   data : pointer to `size` bytes (NOT NUL-terminated, may contain \0)
   size : number of valid bytes at `data`
   return: always 0 (non-zero is reserved by libFuzzer) */
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    /* 1. shape the bytes into what the target expects
       2. call the target
       3. free anything you allocated
       4. return 0 */
    return 0;
}

Build command (all three instrumentations at once):

clang -g -O1 -fsanitize=address,undefined,fuzzer harness.c parser.c -o parser-fuzz
  • -fsanitize=fuzzer links libFuzzer and supplies main.
  • address,undefined enable ASan + UBSan.
  • -g keeps symbols so backtraces are readable; -O1 keeps it fast but debuggable.

Useful run flags:

./parser-fuzz -max_total_time=300      # stop after 300s (CI budget)
./parser-fuzz -max_len=4096 corpus/    # cap input size, seed from corpus/
./parser-fuzz crash-<sha1>             # replay one saved crash

Lesson

Fuzzing throws millions of pseudo-random inputs at a function and watches for crashes.

The standard C entry point is:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)

You write this function so that it calls into the parser you want to exercise.

With ASan and UBSan (Undefined Behavior Sanitizer) enabled, every crash is a real bug to investigate.

Code examples

The shape below is insecure parser → secure parser → verify, with the same harness driving both. The harness stays tiny and correct; the difference is in the function under test.

/* parser.c + harness.c combined for teaching.
 * Build (lab only):
 *   clang -g -O1 -fsanitize=address,undefined,fuzzer this.c -o parser-fuzz
 */
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

/* ----------------------------------------------------------------
 * (1) WARNING: intentionally vulnerable — use only in a local,
 *     isolated, authorized lab. Do not deploy.
 *
 * Bug: assumes the input has at least 4 bytes of "header" and reads
 * len from it, then copies `len` bytes into a fixed 16-byte buffer
 * with NO bound check. A fuzzer finds this in seconds.
 * ---------------------------------------------------------------- */
#ifdef USE_VULNERABLE
bool parse_record(const uint8_t *data, size_t size) {
    char name[16];
    /* insecure assumption: size >= 4, and data[3] <= 16 */
    size_t len = data[3];                 /* over-read if size < 4  */
    memcpy(name, data + 4, len);          /* overflow if len > 16   */
    name[len] = '\0';                     /* off-by-one on top      */
    return name[0] != 'X';
}
#else
/* ----------------------------------------------------------------
 * (2) SECURE fix: validate BEFORE every read. Reject anything that
 *     does not fit. Never read past `size`; never write past the
 *     buffer. This is the defensive parser shape from the prereqs.
 * ---------------------------------------------------------------- */
bool parse_record(const uint8_t *data, size_t size) {
    const size_t HEADER = 4;
    const size_t NAME_CAP = 16;           /* max name length */

    if (size < HEADER) return false;      /* not enough for a header */

    size_t len = data[3];                 /* safe: index 3 < HEADER  */
    if (len > NAME_CAP) return false;     /* reject oversize name    */
    if (len > size - HEADER) return false;/* body must fit in input; */
                                          /* size-HEADER can't underflow, size>=4 */

    char name[NAME_CAP + 1];              /* +1 for the NUL          */
    memcpy(name, data + HEADER, len);     /* len <= NAME_CAP: safe   */
    name[len] = '\0';                     /* index <= NAME_CAP: safe */
    return name[0] != 'X';
}
#endif

/* ----------------------------------------------------------------
 * The HARNESS — identical for both versions of parse_record.
 * Deterministic, no globals, no leaks.
 * ---------------------------------------------------------------- */
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    /* parse_record takes a bounded buffer, so no string copy needed.
     * We pass data/size straight through. If the target needed a C
     * string we would malloc(size+1), memcpy, NUL-terminate, free. */
    parse_record(data, size);
    return 0;
}

A separate, standalone verifier proves the secure version rejects bad input and accepts good input. This is an ordinary unit test you run without the fuzzer — it is your mitigation-verification step:

/* verify.c — build: clang -g -fsanitize=address,undefined verify.c -o verify
 * Links the SECURE parse_record (compiled without USE_VULNERABLE). */
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>

bool parse_record(const uint8_t *data, size_t size);

int main(void) {
    /* REJECT: too short to hold a header */
    uint8_t tiny[] = {1, 2};
    assert(parse_record(tiny, sizeof tiny) == false);

    /* REJECT: header claims len=200, far bigger than input/buffer */
    uint8_t huge[] = {0, 0, 0, 200, 'A'};
    assert(parse_record(huge, sizeof huge) == false);

    /* REJECT: len=5 but only 1 body byte present */
    uint8_t short_body[] = {0, 0, 0, 5, 'A'};
    assert(parse_record(short_body, sizeof short_body) == false);

    /* ACCEPT: 4-byte header, len=3, exactly 3 body bytes 'abc' */
    uint8_t good[] = {0, 0, 0, 3, 'a', 'b', 'c'};
    assert(parse_record(good, sizeof good) == true);

    printf("all mitigation checks passed\n");
    return 0;
}

Expected output of the verifier when the secure parser is correct:

all mitigation checks passed

When you build the harness with -DUSE_VULNERABLE and run ./parser-fuzz, ASan aborts within seconds on a stack-buffer-overflow in parse_record. Rebuild without -DUSE_VULNERABLE (the secure version) and the fuzzer runs its full time budget without a crash — that contrast is the proof the fix holds.

Line by line

Walkthrough of the secure parse_record plus the harness, tracing one hostile input the fuzzer might produce: data = {0, 0, 0, 200, 'A'}, size = 5.

Step Line What happens Values
1 if (size < HEADER) Need 4 header bytes. size is 5, so we continue. size=5, HEADER=4
2 len = data[3] Read the declared name length. Index 3 is inside the header, so this read is safe. len=200
3 if (len > NAME_CAP) 200 > 16, so reject immediately. Return false. returns false

The hostile input is refused before any copy, so no overflow occurs. Now trace a valid input data = {0,0,0,3,'a','b','c'}, size = 7:

Step Line What happens Values
1 size < HEADER 7 >= 4, continue. size=7
2 len = data[3] Name length is 3. len=3
3 len > NAME_CAP 3 <= 16, continue. ok
4 len > size - HEADER size-HEADER = 3; 3 > 3 is false, continue. Body fits. ok
5 memcpy(name, data+4, 3) Copy 'a','b','c' into name. Reads indices 4,5,6 — all < size. name = "abc?"
6 name[3] = '\0' Terminate. Buffer is NAME_CAP+1 = 17 bytes, index 3 is safe. name = "abc"
7 return name[0] != 'X' 'a' != 'X' is true. returns true

Why the ordering matters: check 3 before 4 so size - HEADER (unsigned subtraction) can never underflow — we only reach step 4 knowing size >= HEADER. Each check removes one insecure assumption the vulnerable version made.

The harness side is trivial by design: it forwards data/size and returns 0. Because parse_record already takes a bounded buffer, the harness needs no allocation — which means it also has no cleanup to get wrong and no leak to mis-attribute. When the target does expect a C string, the harness grows to malloc(size+1) → memcpy → copy[size]='\0' → call → free(copy), and every one of those four lines is a place a careless harness introduces its own bug.

Common mistakes

Wrong approach Why it's wrong Corrected How to recognise / prevent
Passing data straight to a function that expects a char * C string data is not NUL-terminated and may contain embedded \0; the target reads past size malloc(size+1), memcpy, set copy[size]='\0', then call, then free ASan reports a heap-buffer-overflow read inside the target on the very first inputs
Forgetting free() in the harness Leak accumulates over millions of runs; libFuzzer blames the parser for a leak the harness caused Free every allocation before return 0 LeakSanitizer report whose stack points into the harness, not the parser
Writing copy[size] into a malloc(size) buffer One-byte heap overflow inside the harness — a false crash Allocate size + 1 bytes ASan heap-buffer-overflow on the terminator line of the harness
Depending on globals, rand(), time(), files, or the network Non-deterministic: crashes don't reproduce, findings are worthless Reset/avoid all shared state; keep the harness pure A saved crash artifact that does not re-crash on replay
Fuzzing without ASan/UBSan Only self-crashing bugs surface; silent corruption slips through Build with -fsanitize=address,undefined,fuzzer Fuzzer "finds nothing" yet manual review later finds overflows
One giant harness for the whole program Shallow coverage, slow, unclear reports One small harness per parser/feature Coverage plateaus early; crashes are hard to attribute
Testing against a live/third-party system "to be realistic" Out of scope, likely unauthorized, and non-reproducible Fuzz the isolated function on localhost only Any target that is not code you own or are authorized to test

Debugging tips

Replay first, always. A crash artifact is a deterministic reproducer:

./parser-fuzz crash-<sha1>

Read the ASan report top-down: the error type (stack-buffer-overflow, heap-use-after-free, …), the operation (READ/WRITE and size), and the first frame in your code in the backtrace — that line is where to look.

Concrete steps when a run crashes:

  1. Confirm it's the parser, not the harness. Look at the top non-sanitizer frame. If it's in LLVMFuzzerTestOneInput, you have a harness bug (usually the size+1 / terminator mistake). Fix that before trusting any finding.
  2. Shrink the input. Let libFuzzer minimise it: ./parser-fuzz -minimize_crash=1 -runs=10000 crash-<sha1>. A smaller reproducer makes the root cause obvious.
  3. Step through under a debugger. gdb --args ./parser-fuzz crash-<sha1>, then run; ASan will stop at the fault so you can inspect len, size, and the buffer.
  4. Add UBSan if only ASan is on. Some bugs (signed overflow feeding a length, invalid shift) are UB that ASan alone won't flag. Rebuild with undefined too.
  5. Turn on a leak check. ASAN_OPTIONS=detect_leaks=1 ./parser-fuzz crash-<sha1> to distinguish a real leak from a normal exit.

Questions to ask when it fails:

  • Is the fault a READ or a WRITE, and how many bytes? (Read past end = missing length check; write past end = missing capacity check.)
  • What was size for the crashing input? Is it below the header size I assumed?
  • Does the crash reproduce on replay? If not, the harness or target is non-deterministic — fix that first, it's a bug in itself.
  • Which validation check should have rejected this input, and why didn't it run before the read?

Memory safety

This is a C, memory-safety-critical topic, so both angles apply.

Memory / UB safety of the harness and target:

  • Allocate size + 1 when you need a NUL-terminated copy; write the terminator at index size, never size into a size-byte buffer.
  • Never index data[i] without first proving i < size. Order your checks so unsigned subtractions like size - HEADER can't underflow.
  • free exactly once, and only what you allocated; set no dangling pointer, reuse no freed buffer. libFuzzer restarts the process on crash, but leaks accumulate across the millions of in-process runs.
  • Keep the harness allocation-free when the target already accepts a bounded (ptr, len) — fewer lines, fewer harness-owned bugs.
  • Handle size == 0: malloc(1) is fine, but calling the target with an empty buffer must be safe; add an early return 0 if the target can't accept it.

Security & safety — detection and logging (for the software you are hardening, not the fuzzer): Fuzzing finds the bug in the lab; in production you also want to detect the hostile input that reaches the deployed parser. When the secure parser rejects input, log a structured security event:

  • Log: timestamp, source identifier (client IP or connection id), the resource/parser that rejected, the result (rejected + reason: too_short, len_exceeds_cap, body_truncated), a correlation id to tie it to the request, and the security decision made.
  • Never log: the raw attacker payload verbatim if it may contain secrets, and never passwords, tokens, session cookies, private keys, full PANs, or unneeded PII. Log a length and a hash, not the bytes, when the content is sensitive.
  • Abuse signals: a spike of too_short / len_exceeds_cap rejections from one source, or many distinct malformed shapes in a short window, suggests someone is probing the parser — the same behaviour your fuzzer produces, now coming from a real client.
  • False positives: legitimate clients on a flaky network, an unusual-but-valid file, or a new client version can trip length checks. Rate-limit and alert on patterns, not single rejections, and keep enough context (correlation id, reason) to tell a bad client from a hostile one.

Correcting a common misconception: a fuzz run that finds no crash in 5 minutes does not prove the parser is secure. Coverage may be shallow, the corpus weak, or the bug behind a check the fuzzer never satisfied. "No crash yet" means "not disproven yet," never "completely secure."

Real-world uses

Authorized real-world use case. You maintain an internal service that parses an uploaded config file. Before release you write one libFuzzer harness around the config parser, seed it with a handful of valid config samples, and run it in CI for five minutes per commit with ASan + UBSan. Week two, it finds a heap-buffer-overflow on a malformed length field. You fix it, add the crashing input to a regression corpus, and re-run to confirm the fix — a bug that would otherwise have been a production crash or a memory-corruption vulnerability is closed in the lab.

Where this happens professionally:

  • OSS-Fuzz — Google's fuzzing-as-a-service continuously fuzzes major open-source projects and files bugs automatically.
  • curl, OpenSSL, SQLite, nginx — all ship fuzz harnesses next to their parsers; new contributions are expected to keep them building.
  • CI gates — many C/C++ shops run a short fuzz budget on every pull request and a long overnight run on a corpus.

Best-practice habits (beginner):

  • One small harness per parser; always build with -fsanitize=address,undefined,fuzzer.
  • Keep the harness deterministic and leak-free; allocate size+1 for string copies.
  • Seed a corpus of real, valid inputs so the fuzzer starts past the first checks.
  • Save every crash artifact with the finding; treat replay as the source of truth.
  • Only ever run against code you own or are explicitly authorized to test, on localhost / a container / an isolated VM.

Best-practice habits (advanced):

  • Use a structured/format-aware input (a FuzzedDataProvider-style splitter) so bytes map cleanly to fields.
  • Maintain a regression corpus of past crashers so fixed bugs can never silently return.
  • Track coverage and add seeds or grammar to break through plateaus.
  • Run multiple sanitizer builds (ASan, then MSan for uninitialised reads) since one build can't catch every class.
  • Apply least privilege and secure defaults in the parser itself: reject-by-default, bounded buffers, explicit length checks — so even inputs the fuzzer never tried are refused.

Practice tasks

All tasks are lab-only: build and run on your own machine / container with -fsanitize=address,undefined,fuzzer. Fuzz only code you wrote or are authorized to test.

Authorization checklist before you start any task: (1) the target is code you own or are explicitly permitted to fuzz; (2) it runs on localhost / a container / an isolated VM with no network target; (3) you have somewhere to store crash artifacts privately; (4) you know the cleanup step below.

Beginner 1 — Wrap a validator.

  • Objective: write LLVMFuzzerTestOneInput around a given int valid_username(const char *s).
  • Requirements: make a NUL-terminated copy of the fuzzer bytes, call the validator, free the copy, return 0. Handle size == 0.
  • Constraints: no globals, no leaks, allocate size + 1.
  • Hints: malloc(size+1), memcpy, copy[size]='\0'. Check malloc for NULL.
  • Concepts: entry point, string-safety of fuzzer bytes, cleanup.

Beginner 2 — Build and run with sanitizers.

  • Objective: compile your harness and run it for 60 seconds.
  • Requirements: clang -g -O1 -fsanitize=address,undefined,fuzzer …; run with -max_total_time=60.
  • Output: either a clean run or a crash-<sha1> artifact.
  • Hints: if it "finds nothing," confirm the sanitizers are actually linked and seed one valid input.
  • Concepts: build flags, time budget, crash artifacts.

Intermediate 1 — Triage and remediate a crash.

  • Objective: given the intentionally-vulnerable parse_record from the lesson (built with -DUSE_VULNERABLE), reproduce the crash and fix it.
  • Requirements: replay the artifact, read the ASan report, identify the missing check, apply the secure version, and re-run to confirm no crash within the same budget.
  • Constraints: change only the parser, not the harness.
  • Hints: -minimize_crash=1 to shrink the input; look for READ vs WRITE and the value of size.
  • Concepts: replay, ASan reports, defensive parser shape.
  • Defensive conclusion: state which insecure assumption caused it, and how the fix removes it.

Intermediate 2 — Prove the fix with a verifier.

  • Objective: write a standalone verify.c (no fuzzer) that asserts the secure parser rejects three bad inputs and accepts one good input.
  • Requirements: cover too-short, oversize-length, and truncated-body cases plus one valid case.
  • Output: all mitigation checks passed.
  • Hints: model it on the lesson's verifier; build with ASan so a hidden bug still aborts.
  • Concepts: mitigation verification, boundary testing.
  • Defensive conclusion: this test is your regression guard — keep it in CI.

Challenge — Structured, deterministic harness with a regression corpus.

  • Objective: fuzz a two-field parser (a length-prefixed name then a numeric flag) using a byte-splitting helper so fuzzer bytes map to fields deterministically.
  • Requirements: no reliance on globals/clock/randomness; save any crashers into a corpus/regressions/ directory and re-run against it as a fast, crash-free replay.
  • Constraints: harness stays leak-free and allocation-correct; secure parser rejects-by-default.
  • Input/output: raw fuzzer bytes → parsed fields or rejection; artifacts on crash.
  • Hints: consume a fixed number of header bytes first, bound each field, then validate before every read.
  • Concepts: format-aware fuzzing, determinism, regression corpus, least privilege.
  • Defensive conclusion: remediate any finding, add its input to the regression corpus, and confirm the corpus replays clean — that closed loop is the deliverable.

Lab cleanup / reset: stop the fuzzer, move or delete crash-* and leak-* artifacts into your private findings folder (do not commit them to a shared/public repo), remove the parser-fuzz binary if you rebuild, and clear any temp corpus you don't intend to keep.

Summary

  • A fuzz harness is the small function int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) that hands one parser to the fuzzer, which generates and mutates millions of inputs under coverage guidance.
  • The bytes in data are not a C string: copy into a size + 1 buffer and NUL-terminate only when the target needs a string; keep the harness deterministic and leak-free so bugs aren't mis-attributed.
  • Build with -fsanitize=address,undefined,fuzzer; sanitizers turn silent corruption into reproducible crashes, and a crash- artifact is your deterministic reproducer — replay it, minimise it, debug it.
  • The defensive core is the parser itself: validate before every read, order checks so unsigned subtraction can't underflow, reject-by-default, and prove the fix with a standalone verifier plus a regression corpus (rejects bad, accepts good).
  • Common mistakes: treating fuzzer bytes as a string, leaking or over-writing in the harness, non-determinism, and fuzzing without sanitizers.
  • Remember: fuzzing is defensive and lab-only — run it against code you own or are authorized to test, on localhost/containers, and clean up artifacts. "No crash yet" never means "secure."

Practice with these exercises