Complete Fuzzing Series - Blog 6 of N
| Part | Title | Status |
|---|---|---|
| 1 | What Is Fuzzing and Why Does It Actually Work? | |
| 2 | How a Fuzzer Works: Mutations, Seeds, and Corpus Explained | |
| 3 | The Taxonomy: Every Type of Fuzzing Explained | |
| 4 | What Happens Inside a Program When It Crashes | |
| 5 | Coverage: The Feedback That Makes Fuzzing Smart | |
| 6 - You are here | AFL++ Installation: Full Setup Guide |
→ Next: Blog 7 - Fuzzing Your First Real Target with AFL++
← Previous: Blog 5 - Coverage: The Feedback That Makes Fuzzing Smart
Part of the Complete Fuzzing Series on IoTSec.in
The first five blogs were theory. Seeds, mutations, corpus, coverage, crashes - all explained, none of it runnable yet.
This one is different.
This is the blog where you actually install AFL++, hit the errors every beginner hits, fix them, and watch the fuzzer find a crash on its own. By the end you will have a working AFL++ setup and a confirmed smoke test - a fuzzer that started with the word hi and figured out FUZZ entirely on its own.
Everything in this blog is documented exactly as it happened. Every command, every error, every fix.
The Environment
Before anything else - know your machine. Everything in this guide was done on:
user@iotsec:~$ uname -a && lsb_release -a
Linux iotsec 6.17.0-35-generic #35~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
Distributor ID: Ubuntu
Description: Ubuntu 24.04.4 LTS
Release: 24.04
Codename: noble
Ubuntu 24.04.4 LTS. Kernel 6.17. x86_64.
This is the current Ubuntu LTS as of 2026. If you are on 22.04 the steps are identical. If you are on something older, the package version might differ but the flow is the same.
Step 1 - Check What You Already Have
Before installing anything, I always check the starting state. It matters for two reasons: you might already have parts of this installed from something else, and it gives you a baseline to compare against after installation.
which afl-fuzz && afl-fuzz --version 2>/dev/null || echo "afl-fuzz not installed"
which clang && clang --version 2>/dev/null || echo "clang not installed"
which afl-clang-fast && afl-clang-fast --version 2>/dev/null || echo "afl-clang-fast not installed"
On a fresh machine you will see three “not installed” lines. That is the correct starting point.
Step 2 - Install AFL++
One command. That is all.
sudo apt install afl++
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
afl++ is already the newest version (4.09c-1ubuntu3).
The following packages were automatically installed and are no longer required:
python3-cliapp python3-markdown python3-ttystatus python3-zombie-imp
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 65 not upgraded.
If you are installing for the first time you will see packages being downloaded and installed instead of “already the newest version.” That line is what you see the second time you run it - meaning everything is already up to date. Do not worry about it.
If you see apt autoremove suggestions at the bottom, ignore them for now. Those are unrelated packages from something else.
What apt is actually installing
Most guides tell you to run the command and move on. I want you to understand what just arrived on your machine.
apt-cache depends afl++
afl++
Depends: build-essential
Depends: procps
Depends: clang-17
Depends: clang
Depends: libc6
Depends: libgcc-s1
Depends: libpython3.12t64
Depends: libstdc++6
Breaks: <afl>
Breaks: <afl++-clang>
Recommends: afl++-doc
Suggests: gnuplot
Breaking this down:
build-essential - gcc, make, and the basic C compilation tools. Required to compile targets.
procps - lets AFL++ monitor processes. This is how AFL++ watches your target program while it runs.
clang-17 - the specific clang version AFL++ was built against. Not the newest clang - a specific one.
clang - the generic clang symlink, pointing at whatever version Ubuntu set as default.
libc6, libgcc-s1, libstdc++6 - standard C and C++ runtime libraries. Almost certainly already on your machine.
libpython3.12t64 - AFL++ uses Python internally for some utilities.
Breaks: afl and Breaks: afl++-clang - the old packages. If you have the original afl installed from years ago, apt will remove it automatically. You do not need to do this manually.
Suggests: gnuplot - optional. If installed, AFL++ can draw coverage graphs over time. Not required for anything in this guide.
The important takeaway: when you run sudo apt install afl++, you do not need to install clang separately. It comes with. One command, everything arrives.
Step 3 - Understand the Clang Version Situation
This confused me when I first set this up. I want to save you the same confusion.
After installation I ran clang --version and got version 18. Then I ran afl-clang-fast --version and got version 17. Different versions. Same machine. I had no idea why.
Here is the full picture:
ls -la /usr/bin/clang* && ls -la /usr/bin/afl-clang*
The important lines from that output:
lrwxrwxrwx /usr/bin/clang -> ../lib/llvm-18/bin/clang
lrwxrwxrwx /usr/bin/clang-17 -> ../lib/llvm-17/bin/clang
lrwxrwxrwx /usr/bin/clang-18 -> ../lib/llvm-18/bin/clang
lrwxrwxrwx /usr/bin/afl-clang-fast -> afl-cc
lrwxrwxrwx /usr/bin/afl-clang-fast++ -> afl-c++
Two things to notice:
First: /usr/bin/clang points to llvm-18. That is just what Ubuntu 24.04 set as the system default. When you type clang, you get version 18.
Second: afl-clang-fast does not point to clang at all. It points to afl-cc - AFL++'s own compiler wrapper. That wrapper internally calls clang-17 (because that is what the afl++ package was built against), but you never see or touch that directly.
This is why the version mismatch does not matter. AFL++ manages its own clang internally. You never need to worry about which version it uses.
clangandafl-clang-fastare not the same tool.clangcompiles code.afl-clang-fastcompiles code and injects AFL++ instrumentation into every branch. Always useafl-clang-fastwhen fuzzing.
Step 4 - Verify the Installation
Three tools. All should report the same version number.
afl-fuzz --version && afl-clang-fast --version && afl-showmap 2>&1 | head -3
afl-fuzz++4.09c
afl-cc++4.09c by Michal Zalewski, Laszlo Szekeres, Marc Heuse - mode: LLVM-PCGUARD
Ubuntu clang version 17.0.6 (9ubuntu1)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-17/bin
afl-showmap++4.09c by Michal Zalewski
afl-showmap [ options ] -- /path/to/target_app [ ... ]
Breaking this down:
afl-fuzz++4.09c - the main fuzzer. Present and working.
afl-cc++4.09c - mode: LLVM-PCGUARD - this confirms afl-clang-fast is actually afl-cc underneath. The mode - LLVM-PCGUARD - is AFL++'s best instrumentation mode. It uses LLVM’s built-in SanitizerCoverage for faster, more accurate tracking. When you see PCGUARD, that is good.
Ubuntu clang version 17.0.6 - confirms afl-clang-fast uses clang-17 internally. Exactly what the dependency tree told us.
afl-showmap++4.09c - the coverage visualization tool. Used to see exactly which edges a specific input triggers. We will use this in later blogs.
Your verified toolkit:
afl-fuzz 4.09c ✅ the fuzzer
afl-clang-fast 4.09c ✅ the instrumented compiler
afl-showmap 4.09c ✅ the coverage inspector
All three same version. All three working. Installation confirmed.
Step 5 - Directory Structure
AFL++ expects a specific folder layout. Before you fuzz anything, you set up three directories.
mkdir -p ~/iotsec/afl-demo/{in,out,crashes}
cd ~/iotsec/afl-demo
.
├── crashes
├── in
└── out
What each folder is for:
in/ - your seeds folder. You put starting inputs here before launching AFL++. AFL++ reads from this folder at startup and never writes to it. At minimum one file must be here before you can run afl-fuzz.
out/ - AFL++'s working directory. AFL++ owns this folder entirely. It creates subfolders inside automatically: out/default/queue/ for corpus files, out/default/crashes/ for crash-causing inputs, out/default/hangs/ for inputs that froze the program. Do not touch this folder during a run.
crashes/ - I created this manually thinking it was where AFL++ saves crashes. It is not. AFL++ creates its own out/default/crashes/ folder automatically. The manual crashes/ folder we created here does nothing.
I am documenting this mistake because I guarantee you will make the same one. When your fuzzer finds a crash, do not look in crashes/. Look in out/default/crashes/.
You create
in/. AFL++ creates everything insideout/. That is the division of responsibility.
Step 6 - Write the Smoke Test Target
A smoke test target is a tiny C program with a deliberately planted crash. The only purpose is to confirm AFL++ works - that it can compile with instrumentation, run, and find the crash on its own.
cat > ~/iotsec/afl-demo/target.c << 'EOF'
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
char buf[64];
int len = fread(buf, 1, sizeof(buf) - 1, stdin);
buf[len] = '\0';
if (len > 3) {
if (buf[0] == 'F') {
if (buf[1] == 'U') {
if (buf[2] == 'Z') {
if (buf[3] == 'Z') {
// intentional crash
abort();
}
}
}
}
}
return 0;
}
EOF
Four nested if checks. The program only crashes if the input is exactly FUZZ. Any other input - the program exits cleanly.
AFL++ does not know FUZZ is in this code. It cannot read the source. It will have to find that combination entirely on its own through mutations.
Step 7 - Compile With Instrumentation
This is the step that separates fuzzing from regular testing.
afl-clang-fast target.c -o target
afl-cc++4.09c by Michal Zalewski, Laszlo Szekeres, Marc Heuse - mode: LLVM-PCGUARD
SanitizerCoveragePCGUARD++4.09c
[+] Instrumented 11 locations with no collisions (non-hardened mode) of which are 4 handled and 0 unhandled selects.
That output is not noise. It is telling you exactly what happened.
mode: LLVM-PCGUARD - the instrumentation engine being used. Modern, fast, accurate.
Instrumented 11 locations - AFL++ planted 11 coverage markers inside your binary. Every branch point, every decision in that small program, got a hidden marker.
no collisions - each marker has a unique ID. No two markers share the same bitmap slot. Clean instrumentation.
4 handled selects - the 4 nested if checks we wrote. AFL++ counted them exactly.
If you had compiled with plain gcc target.c -o target you would get a binary that works but tells nobody anything. afl-clang-fast target.c -o target gives you a binary that works and reports its own execution path to AFL++ after every run.
Normal
gccgives you a binary.afl-clang-fastgives you a binary that talks to AFL++ while it runs.
Step 8 - Create a Seed
A seed is just a valid starting input. It does not need to be clever. It just needs to get past fread and into the program.
echo "hi" > ~/iotsec/afl-demo/in/seed1.txt
hi - two characters. It will fail the len > 3 check immediately and the program exits cleanly. That is fine. The seed does not need to trigger the crash. It just needs to be a starting point for mutations.
Step 9 - Run AFL++ (First Attempt)
afl-fuzz -i in/ -o out/ -- ./target
afl-fuzz++4.09c based on afl by Michal Zalewski and a large online community
[+] No -M/-S set, autoconfiguring for "-S default"
[*] Checking core_pattern...
[-] Hmm, your system is configured to send core dump notifications to an
external utility. This will cause issues: there will be an extended delay
between stumbling upon a crash and having this information relayed to the
fuzzer via the standard waitpid() API.
To avoid having crashes misinterpreted as timeouts, please log in as root
and temporarily modify /proc/sys/kernel/core_pattern, like so:
echo core >/proc/sys/kernel/core_pattern
[-] PROGRAM ABORT : Pipe at the beginning of 'core_pattern'
Location : check_crash_handling(), src/afl-fuzz-init.c:2361
AFL++ refuses to start. Every beginner hits this. Here is what it means.
Error 1 - core_pattern / apport
Check what your system is currently set to:
cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E
That | at the beginning is the pipe character. It means Ubuntu is configured to send crash notifications to apport - Ubuntu’s built-in crash reporter - instead of writing crashes directly to disk.
AFL++ spawns thousands of processes per second. When a process crashes, AFL++ needs to know about it immediately through the standard waitpid() system call. When apport intercepts the crash first, there is a delay. AFL++ cannot distinguish between “the program crashed and apport is handling it” and “the program timed out.” It refuses to run in this ambiguous state.
The fix:
sudo sh -c 'echo core > /proc/sys/kernel/core_pattern'
Is this safe? Yes, completely. Here is why:
/proc/is not a real folder on disk. It is a live view of kernel settings stored in RAM- This change resets automatically on reboot - the moment you restart your machine, Ubuntu restores apport as the crash handler
- Apport still exists and still works - it is just not active until the next reboot
- All this does is tell the kernel “write crashes to disk directly” instead of “pipe them to apport”
Verify the change worked:
cat /proc/sys/kernel/core_pattern
core
No pipe. No apport. The kernel will now write crashes directly to disk and AFL++ can see them instantly.
Step 10 - Run AFL++ (Second Attempt)
afl-fuzz -i in/ -o out/ -- ./target
[*] Checking CPU scaling governor...
[-] Whoops, your system uses on-demand CPU frequency scaling, adjusted
between 2142 and 5230 MHz. Unfortunately, the scaling algorithm in the
kernel is imperfect and can miss the short-lived processes spawned by
afl-fuzz. To keep things moving, run these commands as root:
cd /sys/devices/system/cpu
echo performance | tee cpu*/cpufreq/scaling_governor
[-] PROGRAM ABORT : Suboptimal CPU scaling governor
Location : check_cpu_governor(), src/afl-fuzz-init.c:2470
Another error. Another one every beginner hits.
Error 2 - CPU Scaling Governor
Your CPU uses “on-demand” frequency scaling - it runs at low speed when idle and ramps up when busy. AFL++ spawns thousands of tiny short-lived processes per second. Each one lives only a few milliseconds. The kernel’s scaling algorithm does not ramp up fast enough for processes that short. Result: AFL++ runs slower than it should and timing measurements become unreliable.
AFL++ detects this and refuses to start by default.
You have two options:
Option A - Set CPU to performance mode (proper fix for long sessions):
cd /sys/devices/system/cpu
echo performance | sudo tee cpu*/cpufreq/scaling_governor
This tells the CPU to run at full speed the entire session. Also reverts on reboot. Best choice when you are running AFL++ for hours.
Option B - Skip the check (quick fix for testing):
AFL_SKIP_CPUFREQ=1 afl-fuzz -i in/ -o out/ -- ./target
For a smoke test that runs 60 seconds, skipping the check is the honest choice. Forcing performance mode on a laptop drains battery and runs the CPU hot for no reason during a short test.
We use Option B here:
AFL_SKIP_CPUFREQ=1 afl-fuzz -i in/ -o out/ -- ./target
Step 11 - Watch It Work
AFL++ starts. After about 60 seconds the screen looks like this:
american fuzzy lop ++4.09c {default} (./target) [fast]
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 0 days, 0 hrs, 0 min, 16 sec │ cycles done : 18 │
│ last new find : 0 days, 0 hrs, 0 min, 15 sec │ corpus count : 5 │
│last saved crash : 0 days, 0 hrs, 0 min, 15 sec │saved crashes : 1 │
│ last saved hang : none seen yet │ saved hangs : 0 │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│ now processing : 4.55 (80.0%) │ map density : 33.33% / 55.56% │
│ runs timed out : 0 (0.00%) │ count coverage : 36.20 bits/tuple │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│ now trying : havoc │ favored items : 3 (60.00%) │
│ stage execs : 284/1839 (15.44%) │ new edges on : 5 (100.00%) │
│ total execs : 115k │ total crashes : 425 (1 saved) │
│ exec speed : 7133/sec │ total tmouts : 0 (0 saved) │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│ havoc/splice : 5/115k, 0/0 │ levels : 4 │
│ trim/eff : 74.19%/7, disabled │ pending : 0 │
└─ strategy: explore ────────── state: started :-) ──┘
Press Ctrl+C to stop. Here are the numbers that matter:
saved crashes : 1 - AFL++ found the crash. In 15 seconds. Starting from hi.
total crashes : 425 (1 saved) - it triggered the crash 425 times but only saved 1 unique crash. The rest were duplicates of the same crash hitting the same abort(). AFL++ deduplicates automatically. Only genuinely new crashes get saved.
corpus count : 5 - started with 1 seed, discovered 4 more interesting inputs along the way. Each one unlocked a new if branch on the path to the crash.
levels : 4 - exactly matches our 4 nested if checks. AFL++ had to go 4 levels deep to reach abort().
exec speed : 7133/sec - over 7,000 inputs tested per second. On a single core.
total execs : 115k - 115,000 inputs tested in 16 seconds.
Step 12 - Inspect the Crash
ls out/default/crashes/
id:000000,sig:06,src:000004,time:646,execs:4433,op:havoc,rep:1 README.txt
The crash filename is not random. Every field means something:
id:000000 - first unique crash found (zero-indexed)
sig:06 - killed by signal 6 (SIGABRT - our abort() call)
src:000004 - came from corpus file number 4
time:646 - found at 646 milliseconds into the run
execs:4433 - took 4,433 executions to find
op:havoc - mutation strategy that produced it
rep:1 - reproduction confirmed
README.txt is AFL++ leaving a note inside the crashes folder explaining what it contains. Normal.
Now look at the actual content of the crash file:
xxd out/default/crashes/id:000000*
00000000: 4655 5a5a FUZZ
Four bytes. 46 55 5A 5A. That is F, U, Z, Z in ASCII.
That is the exact input that crashed the program. No newline. Nothing else. Just the 4 bytes that triggered all four if checks and reached abort().
Step 13 - Confirm the Crash is Reproducible
echo "FUZZ" | ./target
echo "Exit code: $?"
Aborted
Exit code: 134
Aborted - the program hit abort() exactly as we wrote it.
Exit code: 134 - that is 128 + 6. In Linux, when a program is killed by a signal, the exit code is 128 + the signal number. Signal 6 is SIGABRT. So 134 means “killed by SIGABRT.” This matches sig:06 in the crash filename exactly.
The crash is real. It is reproducible. AFL++ saved the correct input.
The Part That Should Make You Stop for a Second
AFL++ started with hi. Two characters. It had zero knowledge of the source code. It could not see the if checks. It did not know FUZZ existed anywhere in the program.
This is what actually happened:
Start: "hi" - fails len > 3 check immediately, exits clean
Mutation: "hix9" - passes len check, fails buf[0]=='F'
Mutation: "Fix9" - passes F check → new branch hit → saved to corpus
Mutation: "FUx9" - passes F, U → new branch hit → saved to corpus
Mutation: "FUZx" - passes F, U, Z → new branch hit → saved to corpus
Mutation: "FUZZ" - passes all 4 checks → abort() → CRASH → saved
Each mutation unlocked one new branch. Each saved corpus file became the base for the next round of mutations. The fuzzer built toward the crash one layer at a time without ever knowing what it was looking for.
Total time: 646 milliseconds. Total executions: 4,433.
This was a toy target with 4 checks. Real programs have hundreds of nested conditions across thousands of functions. AFL++ finds those the same way - one branch at a time, building a map of the program it cannot read.
Common Errors and Fixes - Quick Reference
PROGRAM ABORT: Pipe at the beginning of 'core_pattern'
Ubuntu’s apport crash handler is intercepting crashes before AFL++ can see them.
sudo sh -c 'echo core > /proc/sys/kernel/core_pattern'
Reverts on reboot.
PROGRAM ABORT: Suboptimal CPU scaling governor
CPU is using on-demand frequency scaling. Short-lived processes run slower than they should.
For a quick test:
AFL_SKIP_CPUFREQ=1 afl-fuzz -i in/ -o out/ -- ./target
For a proper long session:
cd /sys/devices/system/cpu
echo performance | sudo tee cpu*/cpufreq/scaling_governor
Reverts on reboot.
No such file or directory when running afl-fuzz
Either in/ folder does not exist or it is empty. AFL++ needs at least one seed file in in/ before it can start.
mkdir -p in/
echo "test" > in/seed1.txt
Crashes folder is empty after a run
You are looking in the wrong place. The manual crashes/ folder you created does nothing. AFL++ saves crashes to out/default/crashes/ automatically.
ls out/default/crashes/
afl-clang-fast: command not found
The afl++ package was not installed or the installation failed partway through.
sudo apt update && sudo apt install afl++
What We Learned
afl-fuzz - the main fuzzer binary. Takes a seeds folder (-i), an output folder (-o), and a target program. Runs the feedback loop: mutate → execute → check coverage → keep or discard → repeat.
afl-clang-fast - AFL++'s compiler wrapper. Compiles your target and injects coverage instrumentation at every branch point. Not the same as plain clang. Always use this when building targets for fuzzing.
afl-cc - what afl-clang-fast actually is under the hood. A wrapper that calls clang and adds AFL++ instrumentation on top.
LLVM-PCGUARD - AFL++'s best instrumentation mode. Uses LLVM’s SanitizerCoverage engine. Faster and more accurate than the older afl-gcc mode. When you compile with afl-clang-fast on a modern system this is what you get automatically.
core_pattern - a kernel setting at /proc/sys/kernel/core_pattern that controls where crash dumps go. Ubuntu sets it to pipe crashes to apport by default. AFL++ requires it to be set to core so crashes go directly to disk.
in/ folder - the seeds directory. You own this. Put at least one valid starting input here before running afl-fuzz. AFL++ reads from here at startup and never writes here.
out/default/crashes/ - where AFL++ saves crash-causing inputs. Not the manual crashes/ folder you created. AFL++ creates this automatically inside your output directory.
Crash filename fields - id, sig, src, time, execs, op, rep. Each field tells you something about how and when the crash was found. sig tells you the kill signal - sig:06 is SIGABRT, sig:11 is SIGSEGV.
Exit code 134 - 128 + signal 6 (SIGABRT). How Linux reports that a program was killed by a signal rather than exiting normally.
xxd - hex dump tool. Use this to inspect crash files when the content is not printable text. Crash files from AFL++ are raw bytes - xxd shows you exactly what is in them.
→ Next: Blog 7 - Fuzzing Your First Real Target with AFL++
← Previous: Blog 5 - Coverage: The Feedback That Makes Fuzzing Smart
Part of the Complete Fuzzing Series on IoTSec.in