Safeguard
Developer Tools•

Write your first libFuzzer harness for a C parser

A libFuzzer harness is one function that takes bytes and calls your code. Here is a minimal C example, the build flags, and what the first run looks like.

Safeguard Security Team
4 min read

A libFuzzer harness is a single function, LLVMFuzzerTestOneInput, that receives a byte array and a length and passes them to the code you want to test. Compile it with the fuzzer and AddressSanitizer flags, run the binary, and it starts mutating inputs on its own.

The smallest useful harness

Suppose you have a function that parses a small config format. The harness is a thin wrapper:

#include <stdint.h>
#include <stddef.h>

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

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    parse_config(data, size);
    return 0;
}

LLVM documents the signature as extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size). In C you can drop the extern. The function should return 0, and it must accept any input, including empty and huge ones.

Notice there is no main function. The fuzzing engine supplies it.

Build it

Compile the harness and the code under test with the fuzzer flag, and add AddressSanitizer so memory errors become visible:

clang -g -O1 -fsanitize=fuzzer,address parse_config.c harness.c -o fuzz_parse

The LLVM docs say to use -fsanitize=fuzzer to link the engine and note it is commonly combined with -fsanitize=fuzzer,address. ASan's own page recommends -O1 or higher and -fno-omit-frame-pointer for better stack traces, so add that flag too if your traces look thin.

Run it

Make a folder for the corpus and put one or two small valid config files in it:

mkdir corpus
./fuzz_parse corpus/ -max_total_time=60

-max_total_time caps the run in seconds. Without it, the default is to run indefinitely. Any input that reaches new coverage is written into the corpus folder, so the folder grows as you run.

You will see status lines that begin with a counter and fields such as cov, ft, corp and exec/s. The first is the number of code blocks or edges covered, the last is how many executions run per second. A quick glance at these tells you whether the fuzzer is alive and learning.

When it finds something

If the target crashes, libFuzzer prints the sanitizer report and saves the input to a file named like crash- followed by a hash. Leaks and timeouts get leak- and timeout- names. To replay it, pass the file as the only argument:

./fuzz_parse crash-0a1b2c...

That runs the single input and reproduces the failure, which is exactly what you want when you hand it to a colleague or attach it to a bug.

Three mistakes to avoid on day one

  1. Calling exit or abort inside the harness. A target that terminates the process is reported as a bug, and hides real findings.
  2. Doing slow work per input, like opening files or printing logs. Keep the harness quick so the engine can try many inputs per second.
  3. Forgetting that the input is not null terminated. If your parser expects a C string, copy the bytes into a buffer of size plus one and add the terminator yourself.

Try it yourself

Open the pull request gate demo and notice where a failing check stops a change. A short fuzz run over your saved corpus can be one of those checks.

Click through it at your own pace, or open it full screen.

Quick check

Do I need to write a main function?

No. With the fuzzer flag the engine provides main and calls your function repeatedly.

Why did my fuzzer find nothing in a minute?

Maybe the parser is solid, or the inputs are rejected early. Check that the cov number grows over the run and add a seed file that is valid for your format.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.