Safeguard
Developer Tools•

AFL++ quickstart: instrument, seed, run, and read the screen

A beginner path through AFL++: build with its compiler wrapper, minimize seeds, run afl-fuzz, and read cycles, paths, crashes, map density and stability.

Safeguard Security Team
3 min read

AFL++ fuzzes a program in three steps: compile it with an AFL++ wrapper so it reports coverage, prepare a small seed folder, and run afl-fuzz. The status screen shows progress through counters like cycles done, total paths, unique crashes and stability. Here is what each step needs and what the numbers mean.

Step 1: instrument

The AFL++ documentation recommends LTO mode with clang 11 or newer, then LLVM mode, then the GCC plugin mode as fallbacks. Replace the compiler in your build:

CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --disable-shared
make

Build dependencies as static libraries, not shared ones, so they are instrumented too. For sanitizers, set environment variables such as AFL_USE_ASAN or AFL_USE_UBSAN when building. The docs advise running only one afl-fuzz instance per sanitizer type.

Step 2: prepare seeds

Gather a few small valid inputs. Then use two helper tools. afl-cmin removes inputs that add no new coverage. afl-tmin shrinks each input without changing the path it takes. Fewer, smaller seeds mean faster fuzzing.

Step 3: run

afl-fuzz -i input -o output -- ./target @@

The @@ is replaced with the input file path. Leave it out if the program reads standard input. The docs say to always set a memory limit with -m and to run afl-system-config first on a machine you control. Run it inside screen or tmux, since it runs until you stop it with Ctrl-C.

Persistent mode for speed

The docs describe persistent mode as much faster, reporting a 2x to 20x speedup, but it needs a harness that calls the target functions directly. AFL++ can also build libFuzzer style harnesses, so one harness can serve both tools.

Reading the status screen

The AFL++ documentation explains these fields:

  • cycles done: passes through the queue. Every session should finish at least one.
  • total paths: test cases found that trigger new behavior.
  • uniq crashes and uniq hangs: unique faults and timeouts, saved in the crashes and hangs folders.
  • map density: how much of the coverage bitmap is hit. Very low absolute values suggest a trivial, badly instrumented or early exiting program.
  • stability: how consistently the same input behaves. 100 percent means fully repeatable. Lower values can come from uninitialized memory, leftover temp files, randomness or threads.

A crash counts as unique if its path contains a transition not seen in earlier crashes, so early counts can overstate the number of distinct bugs.

Scaling out

Run one main instance with -M and give each secondary a unique -S name, all sharing the same output folder. Performance drops past roughly 32 to 64 cores on one machine, according to the docs. Secondaries can use different builds, such as a sanitizer build or one with CmpLog.

Quick check

My stability is low. Is the run useless?

Not necessarily. Values above about 90 percent are usually harmless per the docs. Low values mean the fuzzer struggles to tell input effects from noise, so look for randomness, threads or leftover state.

Does one unique crash mean one bug?

No. One bug can be reached by several paths and counted several times. Deduplicate by stack trace when you triage.

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.