Secure Coding in C · intermediate · ~15 min
- Write a function that matches libFuzzer's exact `LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)` signature. - Parse a length-prefixed (TLV) record stream while treating every byte of input as hostile. - Add bounds checks so the parser never reads past `size`, no matter what bytes arrive. - Handle the classic malformed-input cases — empty input, `NULL` data, a length that overruns the buffer, and a zero length — without crashing or looping forever. - Understand why a good fuzz target is deterministic, allocation-clean, and free of global side effects. - Explain how one small function becomes a portable contract that libFuzzer, AFL++, and Honggfuzz can all drive unchanged.
Fuzzing is the practice of feeding a program a flood of random, malformed, and unexpected inputs to see what makes it crash. Each crash is a bug the fuzzer found for you — often a memory-safety bug an attacker could exploit. To use a fuzzer, you write one small function called a fuzz target: it receives a blob of bytes and runs your parsing or decoding logic on them. The fuzzer supplies millions of different blobs per second and watches for anything that goes wrong.
This lesson teaches the shape of that function and the defensive discipline it demands. You already know how to follow a pointer safely from the pointers lesson, and how to guard array and buffer accesses from the bounds-checks lesson. A fuzz target is where those two skills become non-negotiable: the caller (the fuzzer) is deliberately trying to hand you input that breaks your assumptions. If a single access is missing a bounds check, the fuzzer will find it.
We parse a tiny TLV (type-length-value) record format so the logic stays small and the safety reasoning stays visible. We are not launching a live fuzzer inside this lesson's harness; instead we build the function's shape and prove by hand that it survives every hostile input. Once the shape is correct, wiring it to a real fuzzer is a one-line compile step. In plain terms: you write a function that can never be tricked into reading memory it doesn't own — and that is exactly what a fuzz target must be.
Fuzzing is one of the highest-return security tools in modern software. Google's OSS-Fuzz has found tens of thousands of bugs in critical open-source projects — OpenSSL, SQLite, libpng, curl, the Linux kernel's userspace libraries — many of them memory-corruption flaws that could have become remote exploits. The entry point into all of that machinery is the humble LLVMFuzzerTestOneInput function.
The reason the shape matters so much: the signature is a portable contract. Write your logic once behind that signature and libFuzzer, AFL++, and Honggfuzz can each drive it without you changing a line. That is a huge multiplier — you get coverage-guided fuzzing, corpus minimization, crash reproduction, and continuous integration fuzzing essentially for free.
But the contract cuts both ways. A fuzz target that crashes on legitimate malformed input (rather than on a real bug) wastes everyone's time chasing false positives. A target that leaks memory or writes to globals produces flaky, unreproducible results. Learning to write a clean, bounds-safe, side-effect-free target is what separates fuzzing that finds real bugs from fuzzing that finds noise. And the defensive habits you build here — never trust a length field, never advance without checking — are exactly the habits that keep parsers safe in production, not just under a fuzzer.
Definition. A fuzz target is a single function with this exact signature:
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
Plain-language explanation. The fuzzer hands you a pointer to size bytes of input. Your job is to run whatever parsing/decoding logic you want to test on those bytes, then return. The name and types are fixed — the fuzzer's runtime looks up this exact symbol by name at link time and calls it in a tight loop.
How it works internally. When you compile with -fsanitize=fuzzer, the compiler links in a main() provided by the libFuzzer runtime. That main() generates inputs (guided by code coverage), and for each one calls your LLVMFuzzerTestOneInput. Coverage instrumentation records which branches your code took, so the fuzzer learns which inputs reach new code and mutates toward them.
libFuzzer runtime main()
|
| generate/mutate an input blob
v
+-----------------------------+
| data ---> [ b0 b1 b2 ... ] | size = N
+-----------------------------+
| call
v
LLVMFuzzerTestOneInput(data, size) <-- YOUR code
| return 0
v
record coverage, mutate, repeat (millions/sec)
When to use / not use. Use this shape for anything that parses untrusted bytes: file formats, network packets, decompressors, deserializers. Do not put slow I/O, network calls, or randomness inside it — the function is called millions of times and must be fast and deterministic.
Pitfall. Returning anything other than 0 is undefined-by-convention; libFuzzer reserves non-zero returns for future use. Always return 0.
Knowledge check. In your own words, why must
LLVMFuzzerTestOneInputbe deterministic — that is, produce the same behavior every time it sees the samedata/size?
data and size as fully hostileDefinition. Every byte in data, and the value of size itself, is attacker-controlled. There are no guarantees: size can be 0, data can be NULL (when size is 0), and the bytes can be any pattern at all.
Plain-language explanation. In normal code you often assume the caller passes sensible values. Here that assumption is the bug. The whole point of the fuzzer is to violate your assumptions faster than you can imagine them.
How it works internally. The buffer data points to is exactly size bytes and not one byte more. Reading data[size] is an out-of-bounds read — undefined behavior — and under a sanitizer it aborts immediately. The fuzzer is specifically trying to make you read past that boundary.
When to use / not use. Always validate before reading. The rule from the bounds-checks lesson applies to every single access: prove the index is in [0, size) before you dereference.
Pitfall. The most common overrun is reading a multi-byte field near the end of the buffer: you check that pos < size but then read two bytes starting at pos, and the second byte is at pos + 1 == size. Always check that all the bytes you are about to read fit.
Knowledge check (find-the-bug).
if (pos < size) { type = data[pos]; len = data[pos + 1]; }— withsize == 5andpos == 4, which read is out of bounds and why?
Definition. TLV = type-length-value. Our toy record is laid out as:
byte 0 byte 1 bytes 2 .. (len)
+--------+ +--------+ +----------------------+
| len | | type | | payload (len-1) |
+--------+ +--------+ +----------------------+
^ total length of record CONTENTS (type + payload)
len (1 byte): the total number of content bytes in this record — the type byte plus the payload.type (1 byte): a tag. We count records whose type == 0x01.payload (len - 1 bytes): the record's data, which we skip over here.So a whole record occupies 1 + len bytes in the stream: one byte for the len field itself, then len content bytes.
Plain-language explanation. Length-prefixed formats are everywhere — Bluetooth advertisements, TLS records, DNS packets. The parser reads a length, then trusts it to know how far to jump. That trust is precisely where overruns hide: a malicious len says "jump 200 bytes forward" when only 3 bytes remain.
How it works internally. Parsing is a loop with a cursor pos. Each iteration: read len at pos, verify the whole record fits (pos + 1 + len <= size), look at type, then advance pos += 1 + len. The advance uses the attacker's len, so it must be validated first.
When to use / not use. This exact skeleton is reusable for any length-prefixed format. Do not hardcode record sizes — the length field is the whole point of TLV.
Pitfall. A len of 0 means the record has zero content bytes, so pos advances by only 1. If you ever advance by len instead of 1 + len, a zero length advances by 0 and the loop spins forever.
| Malformed input | What a naive parser does | What a defensive parser does |
|---|---|---|
size == 0 |
reads data[0] (overrun) |
returns 0 immediately |
len points past buffer end |
reads payload out of bounds | stops, returns count so far |
len == 0 |
may loop forever | stops (or advances safely by 1) |
| trailing partial record | reads missing bytes | stops when it doesn't fit |
Knowledge check (predict-the-output). Given bytes
02 01 AA 03 01 BB CC(hex), how manytype == 0x01records are there, and where does each record start?
The signature must match exactly — same name, same parameter types, return int. The types come from <stdint.h> (uint8_t) and <stddef.h> (size_t).
#include <stdint.h> /* uint8_t */
#include <stddef.h> /* size_t */
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
/* data: pointer to `size` untrusted bytes (may be NULL when size==0) */
/* size: number of bytes; can be 0 */
size_t pos = 0; /* cursor into the stream */
while (pos < size) { /* only enter if a byte is present */
uint8_t len = data[pos]; /* safe: pos < size proven above */
if (len == 0) break; /* zero length would not advance */
if (pos + 1 + len > size) /* does the whole record fit? */
break; /* no -> stop, don't read past end */
/* ... inspect data[pos + 1] (the type) ... */
pos += 1 + len; /* advance past len byte + content */
}
return 0; /* libFuzzer expects 0 */
}
Two details matter. First, pos + 1 + len is computed in size_t (unsigned), and because len is at most 255 there is no overflow risk against a realistic size — but the order of the check matters: verify the fit before touching data[pos + 1] or the payload. Second, size_t is unsigned, so never write size - 1 without first checking size > 0, or the subtraction wraps to a huge number.
A fuzz target is a single function. The popular fuzzers — libFuzzer, AFL++, and Honggfuzz — all drive their target through the same signature:
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
This signature is a contract. Your function must:
data and size without crashing.0 (libFuzzer ignores any other return value).We are not running a real fuzzer in this harness. Instead, we write the shape of a defensive target and prove it survives difficult inputs.
We use a small toy format. Each record looks like this:
[ len : u8 ][ type : u8 ][ payload : len - 1 bytes ]
len is one byte: the total length of the record's contents.type is one byte: a tag identifying the record.payload is len - 1 bytes (the bytes after type).The fuzz target counts the records whose type == 0x01.
A defensive target has to handle malformed input gracefully:
size == 0 → return 0.data == NULL (with size 0) → return 0.len walks past size → stop counting and return what we have so far. Never read past the end of the buffer.len == 0 → stop. A zero length would never advance, causing an infinite loop.Implement:
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
It should walk the record stream, never read past size, and return the count of type == 0x01 records.
#include <stdint.h> #include <stddef.h> #include <stdio.h> #include <string.h>
/*
lenint count = 0;
size_t pos = 0;
while (pos < size) { /* guaranteed: data[pos] is readable */
uint8_t len = data[pos]; /* content length of this record */
if (len == 0) /* zero-length record cannot advance */
break; /* stop rather than loop forever */
/* Whole record = 1 (len byte) + len (content). Must fit in buffer. */
if (pos + 1 + (size_t)len > size)
break; /* truncated record -> stop, no OOB */
uint8_t type = data[pos + 1];/* safe: fit was proven above */
if (type == 0x01)
count++;
pos += 1 + (size_t)len; /* advance past this record */
}
/*
* A real fuzz target returns 0 and the fuzzer only cares about crashes.
* We keep `count` so a test harness (or a demo main) can observe it.
*/
(void)count;
return 0;
}
/*
int main(void) { /* well-formed: two records, both type 0x01 / const uint8_t good[] = {0x02, 0x01, 0xAA, 0x03, 0x01, 0xBB, 0xCC}; / len (0x09) overruns the 3-byte buffer -> must stop, not read OOB / const uint8_t overrun[] = {0x09, 0x01, 0xAA}; / zero length -> must break, not loop forever */ const uint8_t zero_len[] = {0x00, 0x01, 0x01};
printf("good -> %d type-0x01 records\n", count_type01(good, sizeof good));
printf("overrun -> %d type-0x01 records\n", count_type01(overrun, sizeof overrun));
printf("zero_len -> %d type-0x01 records\n", count_type01(zero_len, sizeof zero_len));
printf("empty -> %d type-0x01 records\n", count_type01(NULL, 0));
/* Also drive the real target signature to show it returns 0 cleanly. */
printf("target(good) returned %d\n", LLVMFuzzerTestOneInput(good, sizeof good));
return 0;
}
/*
We trace the good input {0x02, 0x01, 0xAA, 0x03, 0x01, 0xBB, 0xCC} — 7 bytes — through count_type01 (identical logic to the target, but it returns the count so we can watch it).
data != NULL and size == 7, so we skip the early return.count = 0, pos = 0.pos (0) < 7, enter loop. len = data[0] = 0x02 (this record has 2 content bytes). len != 0. Fit check: pos + 1 + len = 0 + 1 + 2 = 3, and 3 <= 7, so the record fits. type = data[1] = 0x01 → it's a match, count = 1. Advance: pos += 1 + 2 = 3.pos (3) < 7, enter. len = data[3] = 0x03 (3 content bytes). Fit check: 3 + 1 + 3 = 7 <= 7, fits exactly. type = data[4] = 0x01 → match, count = 2. Advance: pos += 1 + 3 = 4, so pos = 7.pos (7) < 7 is false. Loop ends. Return 2.| pos (start) | len = data[pos] | fit? (pos+1+len ≤ size) | type = data[pos+1] | count | new pos |
|---|---|---|---|---|---|
| 0 | 0x02 | 3 ≤ 7 ✓ | 0x01 | 1 | 3 |
| 3 | 0x03 | 7 ≤ 7 ✓ | 0x01 | 2 | 7 |
| 7 | — | loop exits (7 < 7 false) | — | 2 | — |
Now trace the overrun input {0x09, 0x01, 0xAA} — 3 bytes:
data != NULL, size == 3.pos (0) < 3. len = data[0] = 0x09 (claims 9 content bytes). len != 0. Fit check: 0 + 1 + 9 = 10, and 10 <= 3 is false → the record does not fit → break. We never read data[1..9], so no out-of-bounds access. Return 0.The attacker put a large len hoping we'd trust it and read 9 bytes from a 3-byte buffer. The fit check catches exactly that. For zero_len {0x00, ...}, the very first len = 0x00 triggers if (len == 0) break;, so we stop before any infinite loop. For the empty case, size == 0 returns immediately.
Mistake 1 — checking the length field after reading past it.
/* WRONG: reads type before confirming the record fits */
uint8_t len = data[pos];
uint8_t type = data[pos + 1]; /* pos+1 may already be == size */
if (pos + 1 + len <= size) { ... } /* too late */
Why it's wrong: with size == 1 and pos == 0, data[pos + 1] is data[1], one byte past the end — an out-of-bounds read the fuzzer will find instantly. Fix: prove the whole record fits before touching any byte beyond data[pos]:
uint8_t len = data[pos];
if (len == 0 || pos + 1 + (size_t)len > size) break;
uint8_t type = data[pos + 1]; /* now guaranteed safe */
Recognize it by asking, for every data[...], "what proved this index is < size?"
Mistake 2 — advancing by len instead of 1 + len.
pos += len; /* WRONG: forgets the len byte itself */
Why it's wrong: two failures at once. It miscounts record boundaries, and when len == 0 it advances by 0 — an infinite loop that hangs the fuzzer (which reports it as a timeout crash). Fix: pos += 1 + (size_t)len; and always guard len == 0 separately.
Mistake 3 — size - 1 without checking size > 0.
for (size_t i = 0; i < size - 1; i++) { ... } /* WRONG when size == 0 */
Why it's wrong: size is unsigned (size_t). When size == 0, size - 1 wraps to SIZE_MAX (a gigantic number), so the loop runs almost forever and reads wildly out of bounds. Fix: guard if (size == 0) return 0; first, or restructure as i + 1 < size.
Mistake 4 — assuming data is a valid pointer when size == 0.
libFuzzer may pass data == NULL with size == 0. Dereferencing it crashes. Fix: the if (data == NULL || size == 0) return 0; guard at the top.
Compiler errors.
unknown type name 'uint8_t' / 'size_t' — you forgot #include <stdint.h> and/or #include <stddef.h>.comparison of integer expressions of different signedness usually means you're comparing a signed int cursor with an unsigned size — make the cursor size_t.Runtime errors (what a sanitizer reports).
heap-buffer-overflow READ of size 1 from AddressSanitizer means you read a byte past data + size. Look at the access it names and ask which check should have proven that index in range. Build with -fsanitize=address,undefined -g so the report includes line numbers.timeout or out-of-memory report often means an infinite loop (forgot the len == 0 guard) or an allocation inside the target that isn't freed.Logic errors.
pos, len, and type each iteration and hand-check against your input bytes, exactly like the trace table above.1 + len bytes, not len.Reproducing a fuzzer crash. libFuzzer writes the crashing input to a file like crash-<hash>. Re-run your target on just that file (./target crash-<hash>) under a debugger to step through it.
Questions to ask when it doesn't work. Did I check the fit before every multi-byte read? Is my cursor unsigned? Do I advance by 1 + len? Is there any input length that makes an arithmetic expression wrap? Does size == 0 reach a data[...] access?
This lesson is a memory-safety lesson: a fuzz target's entire reason to exist is to expose undefined behavior. The concerns for this topic:
data[i] must be provably in [0, size). The defensive pattern is: check the index (or the whole span you're about to read) before the access, never after. Reading even one byte past size is undefined behavior and, under AddressSanitizer, an immediate abort.size and pos are size_t (unsigned). Subtractions like size - 1 or size - needed wrap to enormous values when the left side is smaller. Always establish size >= needed before subtracting, or rewrite as addition (pos + needed <= size).pos + 1 + len could in theory overflow size_t, but since len <= 255 and pos <= size, it's safe for any realistic buffer. If a format allowed multi-byte lengths, you would need an explicit overflow-safe check such as len <= size - pos - 1 (with size - pos - 1 guarded to be non-negative).0) is a denial-of-service. Guard it and always make forward progress.data may be NULL when size == 0; guard it.Defensive-security framing. These are the same bugs attackers weaponize in real parsers — a trusted length field is the classic vector behind Heartbleed-style over-reads. Writing the fit check correctly here is the defensive habit that prevents that class of vulnerability. This target only counts records; it never emits or acts on attacker data, so there is nothing to weaponize. That is the right posture: fuzz targets find crashes; they do not exploit them.
Where this shows up. Google's OSS-Fuzz continuously fuzzes hundreds of open-source projects, and every one of them exposes exactly this LLVMFuzzerTestOneInput entry point. Image libraries (libpng, libjpeg-turbo), crypto (OpenSSL, BoringSSL), databases (SQLite), compression (zlib, brotli), and protocol parsers (curl, c-ares) all ship fuzz targets shaped precisely like the one in this lesson. When a target finds a crash, the project gets an automatic bug report with a reproducer input — often before the code ever ships.
Beginner best practices.
return 0.NULL/size == 0 guard first.pos, len, count) so the safety reasoning is legible.Advanced best practices.
LLVMFuzzerInitialize for one-time setup (never per-call work).FuzzedDataProvider to carve typed values out of the raw bytes cleanly.-fsanitize=address,undefined (and MemorySanitizer for uninitialized-read detection) in CI so any regression is caught immediately.Beginner 1 — Enough input?
Write int enough_input(size_t size, size_t needed) that returns 1 if size >= needed, else 0. Requirements: no subtraction (avoid unsigned wraparound), no other logic. Example: enough_input(4, 8) → 0; enough_input(8, 8) → 1. Concepts: size_t comparison, the length-guard habit. Hint: it is a single comparison.
Beginner 2 — Bounds-safe byte read.
Write int safe_byte(const unsigned char *data, size_t size, size_t idx) that returns data[idx] (0–255) if idx < size, otherwise returns -1. Requirements: never read out of bounds; handle data == NULL by returning -1. Example: for {0x41, 0x42}, safe_byte(d, 2, 1) → 66, safe_byte(d, 2, 5) → -1. Concepts: bounds check before dereference.
Intermediate 1 — Count a specific type.
Starting from the lesson's target, write a version that counts records whose type equals a value you choose (say 0x02) and returns that count from a helper. Requirements: reuse the fit check and len == 0 guard; test it against {0x02, 0x02, 0xAA, 0x02, 0x01, 0xBB}. Expected: one type == 0x02 record. Concepts: TLV walking, bounds checks.
Intermediate 2 — Total payload bytes.
Write a target-shaped function that, instead of counting, sums the total payload length (len - 1 per record) across all well-formed records and stops safely at the first truncated one. Requirements: never read past size; len == 0 stops the walk; return 0 from the target but expose the sum via a helper for testing. Example: {0x02, 0x01, 0xAA, 0x03, 0x01, 0xBB, 0xCC} → payload total 1 + 2 = 3. Concepts: arithmetic on validated lengths, size_t safety.
Challenge — Two-byte length field.
Extend the format so len is a two-byte, big-endian field (len = (data[pos] << 8) | data[pos+1]), followed by type and payload. Write a bounds-safe target that walks this stream. Requirements: read the two length bytes only after proving pos + 2 <= size; then prove the whole record (2 + len bytes) fits before advancing; guard against len == 0; and write the fit check so it cannot wrap even though len can now be up to 65535. Hint: prefer len > size - pos - 2 after checking size - pos >= 2, rather than pos + 2 + len > size, to reason clearly about overflow. Concepts: multi-byte fields, overflow-safe bounds checks, endianness.
A fuzz target is one small function with a fixed signature — int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) — and matching that shape turns your parser into something libFuzzer, AFL++, and Honggfuzz can all drive unchanged. The function must treat every byte of data and the value of size as hostile: guard NULL/size == 0, bounds-check every access before reading, advance a cursor by the full record size (1 + len), and never trust a length field without proving the whole record fits.
The most important syntax is the signature itself plus the fit check if (pos + 1 + (size_t)len > size) break; and the if (len == 0) break; guard. The most common mistakes are reading a multi-byte field before checking it fits, advancing by len instead of 1 + len (which loops forever on a zero length), and writing size - 1 when size might be 0 (unsigned wraparound). Remember: keep the target deterministic, allocation-clean, and free of global writes, and always return 0. Master these bounds habits here and you have the exact discipline that keeps real-world parsers safe from the length-field over-reads attackers love to exploit.