Coverage-Guided Fuzzing with AFL++: Instrumentation, Crash Analysis, and Memory Safety Case Studies
| Fuzzing | AFL Exercises on GitHub

Coverage-Guided Fuzzing with AFL++: Instrumentation, Crash Analysis, and Memory Safety Case Studies

TCPdump

Original writeup on GitHub

Getting the Target Ready

Lets start by setting up a clean workspace for fuzzing tcpdump.

cd $HOME
mkdir fuzzing_tcpdump && cd fuzzing_tcpdump/

Lets then fetch and extract the tcpdump source code.

wget https://github.com/the-tcpdump-group/tcpdump/archive/refs/tags/tcpdump-4.9.2.tar.gz
tar -xzvf tcpdump-4.9.2.tar.gz

tcpdump depends on libpcap for packet parsing, so lets grab that next.

wget https://www.tcpdump.org/release/libpcap-1.8.0.tar.gz
tar -xzvf libpcap-1.8.0.tar.gz

Lets then configure and build libpcap with shared libraries disabled.

cd $HOME/fuzzing_tcpdump/libpcap-1.8.0/
./configure --enable-shared=no
make

image

And yey : )

Now that the setup is complete, lets move on to generating an initial corpus for fuzzing

image

ASan

AddressSanitizer (ASan) is a fast, compiler-based memory error detector built into Clang that helps catch critical memory safety bugs at runtime. By instrumenting the program during compilation and linking it with a lightweight runtime, ASan can reliably detect issues such as heap and stack out-of-bounds accesses, use-after-free, use-after-return, double frees, and memory leaks with a relatively low performance overhead of about 2x. Because it stops execution on the first detected error and produces precise, symbolized stack traces, AddressSanitizer is especially useful when compiling fuzzing targets, where crashing early and deterministically is more valuable than continuing execution in a corrupted state

Building tcpdump and libpcap with AddressSanitizer Enabled

Now that the target and its dependencies are set up, the next step is to rebuild both libpcap and tcpdump with AddressSanitizer (ASan) enabled. This is a crucial step for fuzzing, as ASan allows us to catch memory safety bugs such as heap overflows, use after free, and use after return immediately when they occur, instead of letting the program continue in a corrupted state.

Cleaning Previous Builds

Before enabling ASan, it is important to remove any artifacts produced by earlier, non-instrumented builds. Mixing sanitized and non-sanitized objects often leads to subtle issues and unreliable results during fuzzing.

We begin by deleting the previous installation prefix and cleaning both source trees.

rm -r $HOME/fuzzing_tcpdump/install
cd $HOME/fuzzing_tcpdump/libpcap-1.8.0/
make clean
cd $HOME/fuzzing_tcpdump/tcpdump-tcpdump-4.9.2/
make clean

At this point, both projects are back to a pristine state and ready to be rebuilt with instrumentation enabled.

Building libpcap with ASan and AFL++

We first rebuild libpcap, since tcpdump links against it. For fuzzing, we want libpcap to be instrumented as well so that memory bugs inside packet parsing code are detectable.

We explicitly select the AFL++ LLVM-based compiler wrapper and configure libpcap to install into our isolated prefix.

cd $HOME/fuzzing_tcpdump/libpcap-1.8.0/
export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure \
  --enable-shared=no \
  --prefix="$HOME/fuzzing_tcpdump/install/"

The --enable-shared=no option ensures that a static libpcap is built, which avoids runtime issues when combining ASan with dynamically linked libraries during fuzzing.

We then compile libpcap with ASan enabled by setting AFL_USE_ASAN=1:

AFL_USE_ASAN=1 make

This causes AFL++ to automatically add the required -fsanitize=address flags and link against the AddressSanitizer runtime.

Building tcpdump with ASan and AFL++

With libpcap built and installed into our custom prefix, we can now build tcpdump itself. As before, we use the AFL++ compiler wrapper and point the build system at the instrumented libpcap.

cd $HOME/fuzzing_tcpdump/tcpdump-tcpdump-4.9.2/
AFL_USE_ASAN=1 CC=afl-clang-lto ./configure \
  --prefix="$HOME/fuzzing_tcpdump/install/"

Once configured, we compile and install tcpdump with ASan enabled:

AFL_USE_ASAN=1 make
AFL_USE_ASAN=1 make install

At the end of this step, both tcpdump and libpcap are fully instrumented with AddressSanitizer and AFL++ coverage instrumentation. This gives us a high-signal fuzzing target where memory safety violations result in immediate, high-quality crashes that are easy to triage and reproduce.

AFL++ Refusing to Start: A Beginner-Friendly Explanation

After building tcpdump and libpcap with AddressSanitizer and AFL++ instrumentation, the natural next step is to start fuzzing. Everything looks correct, the command is ready, and then… AFL++ suddenly aborts with a scary error message about core_pattern.

image

AFL++ runs your program thousands of times per second with slightly broken inputs, hoping the program will crash. When a crash happens, AFL++ needs to know immediately so it can:

  • Save the crashing input
  • Mark it as a real bug
  • Move on to the next mutation

What Is core_pattern?

On Linux, when a program crashes, the kernel follows a rule stored in:

/proc/sys/kernel/core_pattern

This rule tells the system what to do when a crash happens.

On many modern systems, this rule sends crash information to another program (like a crash reporter)

Why ?

When crashes are sent to an external program first, AFL++ does not hear about the crash instantly. That means:

  • A real crash might look like a timeout
  • The crash might be delayed or missed
  • Fuzzing results become unreliable

Because of this, AFL++ checks your system setup at startup. If it sees that crashes are being “piped away”, it refuses to run and aborts with an error.

The Fix

We tell Linux to report crashes directly instead of sending them to another program.

Run this once:

echo core | sudo tee /proc/sys/kernel/core_pattern

The Lazy shit

If you are just playing around and do not care about missing crashes, you can tell AFL++ to ignore the problem:

export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1

Moving past that Let's start AFL and get fuzzing with the ASn enabled

image

Some explanation on Adress San internals .

For a small demo we i'll try explaining it through a program to demo the leak finding using clang sanitizer .

image

muffin@muffinn:/mnt/d/pwn$ clang -fsanitize=address -g memory-leak.c ; ASAN_OPTION=detect_leaks=1 ./a.out

=================================================================
==228472==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 7 byte(s) in 1 object(s) allocated from:
    #0 0x62e047f2c193 in malloc (/mnt/d/pwn/a.out+0xc6193) (BuildId: f2f2c331c2931debd20029b6da7b5dccfbfbfe97)
    #1 0x62e047f6a748 in main /mnt/d/pwn/memory-leak.c:8:6
    #2 0x7ce7f622a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #3 0x7ce7f622a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #4 0x62e047e91344 in _start (/mnt/d/pwn/a.out+0x2b344) (BuildId: f2f2c331c2931debd20029b6da7b5dccfbfbfe97)

SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

AddressSanitizer exits on the first detected error. This is by design

Xpdf

Original writeup on GitHub

First lets set up the enviorment

image

Lets quickly get out target which we are going to fuzz

image

wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz

And lets build it :3

cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

image

Before starting AFL , we'll need some samples to verify so lets get them too

image

And voila everything is cleared up

muffin@muffinn:~/fuzzing_xpdf/pdf_examples$ $HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf
Tagged:         no
Pages:          1
Encrypted:      no
Page size:      200 x 200 pts
MediaBox:           0.00     0.00   200.00   200.00
CropBox:            0.00     0.00   200.00   200.00
BleedBox:           0.00     0.00   200.00   200.00
TrimBox:            0.00     0.00   200.00   200.00
ArtBox:             0.00     0.00   200.00   200.00
File size:      678 bytes
Optimized:      no
PDF version:    1.7

Installing AFL

muffin@muffinn:~/fuzzing_xpdf/pdf_examples$ sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev

Building AFL

image

zzz

muffin@muffinn:~/AFLplusplus$ afl-fuzz
afl-fuzz++4.35c based on afl by Michal Zalewski and a large online community

afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]

Required parameters:
  -i dir        - input directory with test cases (or '-' to resume, also see
                  AFL_AUTORESUME)
  -o dir        - output directory for fuzzer findings

Execution control settings:
  -P strategy   - set fix mutation strategy: explore (focus on new coverage),
                  exploit (focus on triggering crashes). You can also set a
                  number of seconds after without any finds it switches to
                  exploit mode, and back on new coverage (default: 1000)
  -p schedule   - power schedules compute a seed's performance score:
                  explore(default), fast, exploit, seek, rare, mmopt, coe, lin
                  quad -- see docs/FAQ.md for more information
  -f file       - location read by the fuzzed program (default: stdin or @@)
  -t msec       - timeout for each run (auto-scaled, default 1000 ms). Add a '+'
                  to auto-calculate the timeout, the value being the maximum.
  -m megs       - memory limit for child process (0 MB, 0 = no limit [default])
  -O            - use binary-only instrumentation (FRIDA mode)
  -Q            - use binary-only instrumentation (QEMU mode)
  -U            - use unicorn-based instrumentation (Unicorn mode)
  -W            - use qemu-based instrumentation with Wine (Wine mode)
  -X            - use VM fuzzing (NYX mode - standalone mode)
  -Y            - use VM fuzzing (NYX mode - multiple instances mode)
  -K dir        - use python script to interact with GUI (GUI mode)

AFL++ is a coverage-guided fuzzer that repeatedly runs a target program with mutated inputs and watches which code paths get executed. It instruments the program to track basic-block or edge coverage, keeps inputs that discover new paths, and mutates those “interesting” inputs more aggressively. Over time, this feedback loop lets AFL++ explore deeper logic, automatically surfacing crashes, hangs, and weird edge cases without knowing anything about the program’s internals.

Now for fuzzing we need to build the application with afl-clang-fast , so that it adds the nessecary things needed for AFL , below are some examples .

image

AFL technical details documentation

One of the reasons which make AFL so fast is how it manages process execution, primarily through fork server mode and persistent mode.

image

Fork Server Mode

Fork server mode is the default execution model in AFL-style fuzzers.

When the instrumented program starts, AFL++ runs it once and stops execution at main(). From this point onward, instead of repeatedly launching the program from scratch using execve, AFL++ uses fork() to create lightweight child processes. Each child:

  • Receives a mutated input
  • Executes the target logic once
  • Exits immediately after processing

Because program initialization happens only once, this avoids repeated startup costs and delivers a massive speedup compared to naive fuzzing. However, each iteration still involves a fork and process teardown, which can become expensive for very tight fuzz loops or heavy parsers.

Persistent Mode

Persistent mode pushes performance even further by minimizing process creation entirely.

In persistent mode, the target program stays alive and processes multiple fuzz inputs within a single process instance. Instead of forking for every test case, AFL++ repeatedly feeds new inputs into a loop inside the program. This is especially effective when:

  • Initialization is expensive
  • The target logic can be cleanly re-entered
  • State can be reset between iterations

Writing a Persistent Mode Harness

To enable persistent mode, you typically write a small harness that reads input and processes it inside an __AFL_LOOP().

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>

#define MAX_INPUT 1024

void process_input(const uint8_t *data, size_t size) {
    if (size > 4 && memcmp(data, "muffin!", 4) == 0) {
        char buf[8];
        memcpy(buf, data, size); // intentional bug erm 
    }
}

int main(void) {
    uint8_t buf[MAX_INPUT];

    while (__AFL_LOOP(1000)) {
        ssize_t len = read(0, buf, sizeof(buf));
        if (len <= 0) break;

        process_input(buf, len);
    }

    return 0;
}

Here, __AFL_LOOP() tells AFL++ to repeatedly execute the target logic without restarting the process. The argument controls how many iterations occur before AFL++ allows a restart to mitigate memory leaks or corrupted state.

Back on track lets build with afl's clang

export LLVM_CONFIG="llvm-config-11"
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

And voila we have it ;

image

Now lets run AFL hooked on to the target ;

image

Since AFL++ depends on immediate waitpid() feedback to reliably detect crashes, it refuses to run in this setup. The proper fix is to temporarily disable crash piping by running echo core | sudo tee /proc/sys/kernel/core_pattern and then rerunning afl-fuzz; this ensures crashes are reported directly and accurately. So lets do that ?

image

After one hour we get some results : 3

image

what we are interested is in what corpus input lead to the crash ? So let's check it out .

image

muffin@muffinn:~/fuzzing_xpdf/out/default/crashes$ ls
README.txt  id:000000,sig:11,src:001506,time:3029564,execs:1014647,op:havoc,rep:4  id:000001,sig:11,src:000869,time:4945976,execs:1748639,op:havoc,rep:1

Now lets try seeing them and get into it deeply .

Lets pick a crash file and feed it into the binary

Great we hit a segfault

image

Lets use gdb to trace it back to see what exactly is going on behind the scenes .

We'll first rebuild this with a stack trace to see or have more symbols present in the assembly mess :((

rm -rf $HOME/fuzzing_xpdf/install

cd $HOME/fuzzing_xpdf/xpdf-3.02
make distclean || true

CFLAGS="-g -O0" CXXFLAGS="-g -O0" \
./configure --prefix="$HOME/fuzzing_xpdf/install"

make -j$(nproc)
make install

And yea

muffin@muffinn:~/fuzzing_xpdf/xpdf-3.02$ file $HOME/fuzzing_xpdf/install/bin/pdftotext
/home/muffin/fuzzing_xpdf/install/bin/pdftotext: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c6ca250d6c2adefb20df63d3be125d96909c6424, for GNU/Linux 3.2.0, with debug_info, not stripped
gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext \
$HOME/fuzzing_xpdf/out/default/crashes/id:000000,sig:11,src:001506,time:3029564,execs:1014647,op:havoc,rep:4 \
$HOME/fuzzing_xpdf/output

image

since it crashed , we'll use bt for the execution history of the program and see the function calls from current to wherever it started .

#60804 0x0000555555600ec7 in Parser::getObj (this=0x5555558d9900, obj=0x7fffffef4f00, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60805 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef4f00) at XRef.cc:823
#60806 0x00005555555fbdd6 in Object::fetch (this=0x5555558d92f0, xref=0x5555556ce630, obj=0x7fffffef4f00) at Object.cc:106
#60807 0x000055555559cfe4 in Dict::lookup (this=0x5555558d96b0, key=0x55555564fa6f "Length", obj=0x7fffffef4f00) at Dict.cc:76
#60808 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef51d0, key=0x55555564fa6f "Length", obj=0x7fffffef4f00) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60809 0x0000555555601337 in Parser::makeStream (this=0x5555558d93a0, dict=0x7fffffef51d0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60810 0x0000555555600ec7 in Parser::getObj (this=0x5555558d93a0, obj=0x7fffffef51d0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60811 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef51d0) at XRef.cc:823
#60812 0x00005555555fbdd6 in Object::fetch (this=0x5555558d8d90, xref=0x5555556ce630, obj=0x7fffffef51d0) at Object.cc:106
#60813 0x000055555559cfe4 in Dict::lookup (this=0x5555558d9150, key=0x55555564fa6f "Length", obj=0x7fffffef51d0) at Dict.cc:76
#60814 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef54a0, key=0x55555564fa6f "Length", obj=0x7fffffef51d0) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60815 0x0000555555601337 in Parser::makeStream (this=0x5555558d8e40, dict=0x7fffffef54a0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60816 0x0000555555600ec7 in Parser::getObj (this=0x5555558d8e40, obj=0x7fffffef54a0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60817 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef54a0) at XRef.cc:823
#60818 0x00005555555fbdd6 in Object::fetch (this=0x5555558d8830, xref=0x5555556ce630, obj=0x7fffffef54a0) at Object.cc:106
#60819 0x000055555559cfe4 in Dict::lookup (this=0x5555558d8bf0, key=0x55555564fa6f "Length", obj=0x7fffffef54a0) at Dict.cc:76
#60820 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef5770, key=0x55555564fa6f "Length", obj=0x7fffffef54a0) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60821 0x0000555555601337 in Parser::makeStream (this=0x5555558d88e0, dict=0x7fffffef5770, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60822 0x0000555555600ec7 in Parser::getObj (this=0x5555558d88e0, obj=0x7fffffef5770, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60823 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef5770) at XRef.cc:823
#60824 0x00005555555fbdd6 in Object::fetch (this=0x5555558d82d0, xref=0x5555556ce630, obj=0x7fffffef5770) at Object.cc:106
#60825 0x000055555559cfe4 in Dict::lookup (this=0x5555558d8690, key=0x55555564fa6f "Length", obj=0x7fffffef5770) at Dict.cc:76
#60826 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef5a40, key=0x55555564fa6f "Length", obj=0x7fffffef5770) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60827 0x0000555555601337 in Parser::makeStream (this=0x5555558d8380, dict=0x7fffffef5a40, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60828 0x0000555555600ec7 in Parser::getObj (this=0x5555558d8380, obj=0x7fffffef5a40, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60829 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef5a40) at XRef.cc:823
#60830 0x00005555555fbdd6 in Object::fetch (this=0x5555558d7d70, xref=0x5555556ce630, obj=0x7fffffef5a40) at Object.cc:106
#60831 0x000055555559cfe4 in Dict::lookup (this=0x5555558d8130, key=0x55555564fa6f "Length", obj=0x7fffffef5a40) at Dict.cc:76
#60832 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef5d10, key=0x55555564fa6f "Length", obj=0x7fffffef5a40) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60833 0x0000555555601337 in Parser::makeStream (this=0x5555558d7e20, dict=0x7fffffef5d10, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60834 0x0000555555600ec7 in Parser::getObj (this=0x5555558d7e20, obj=0x7fffffef5d10, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60835 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef5d10) at XRef.cc:823
#60836 0x00005555555fbdd6 in Object::fetch (this=0x5555558d7810, xref=0x5555556ce630, obj=0x7fffffef5d10) at Object.cc:106
#60837 0x000055555559cfe4 in Dict::lookup (this=0x5555558d7bd0, key=0x55555564fa6f "Length", obj=0x7fffffef5d10) at Dict.cc:76
#60838 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef5fe0, key=0x55555564fa6f "Length", obj=0x7fffffef5d10) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60839 0x0000555555601337 in Parser::makeStream (this=0x5555558d78c0, dict=0x7fffffef5fe0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60840 0x0000555555600ec7 in Parser::getObj (this=0x5555558d78c0, obj=0x7fffffef5fe0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60841 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef5fe0) at XRef.cc:823
#60842 0x00005555555fbdd6 in Object::fetch (this=0x5555558d72b0, xref=0x5555556ce630, obj=0x7fffffef5fe0) at Object.cc:106
#60843 0x000055555559cfe4 in Dict::lookup (this=0x5555558d7670, key=0x55555564fa6f "Length", obj=0x7fffffef5fe0) at Dict.cc:76
#60844 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef62b0, key=0x55555564fa6f "Length", obj=0x7fffffef5fe0) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60845 0x0000555555601337 in Parser::makeStream (this=0x5555558d7360, dict=0x7fffffef62b0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60846 0x0000555555600ec7 in Parser::getObj (this=0x5555558d7360, obj=0x7fffffef62b0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60847 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef62b0) at XRef.cc:823
#60848 0x00005555555fbdd6 in Object::fetch (this=0x5555558d6d50, xref=0x5555556ce630, obj=0x7fffffef62b0) at Object.cc:106
#60849 0x000055555559cfe4 in Dict::lookup (this=0x5555558d7110, key=0x55555564fa6f "Length", obj=0x7fffffef62b0) at Dict.cc:76
#60850 0x00005555555fcaad in Object::dictLookup (this=0x7fffffef6580, key=0x55555564fa6f "Length", obj=0x7fffffef62b0) at /home/muffin/fuzzing_xpdf/xpdf-3.02/xpdf/Object.h:253
#60851 0x0000555555601337 in Parser::makeStream (this=0x5555558d6e00, dict=0x7fffffef6580, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:156
#60852 0x0000555555600ec7 in Parser::getObj (this=0x5555558d6e00, obj=0x7fffffef6580, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0x0, objNum=0x4, objGen=0x0) at Parser.cc:94
#60853 0x0000555555625951 in XRef::fetch (this=0x5555556ce630, num=0x4, gen=0x0, obj=0x7fffffef6580) at XRef.cc:823

This was a small snippet but , the important part is here that , this is an infinite loop, more precisely infinite recursion .

And the repeating cycle being

Parser::getObj
→ Parser::makeStream
→ Object::dictLookup ("Length")
→ Dict::lookup
→ Object::fetch
→ XRef::fetch
→ Parser::getObj

So the parser is trying to resolve the same PDF object again and again, without any termination condition.

This bug exists because xpdf does not detect recursive object resolution. When /Length points back (directly or indirectly) to the same object, the parser keeps resolving forever.

You must track visited objects during resolution and bail out if the same object is seen again.

Conceptually:

  • Keep a set of (objNum, objGen) currently being resolved
  • Before resolving an object, check if it’s already in the set
  • If yes - error out instead of recursing

Example patch idea

In Parser::getObj() or near XRef::fetch():

static std::set<std::pair<int,int>> resolving;

auto key = std::make_pair(objNum, objGen);

if (resolving.count(key)) {
    error(errSyntaxError, -1, "Recursive object reference detected");
    obj->initNull();
    return;
}

resolving.insert(key);

// existing parsing logic here

resolving.erase(key);

This completely kills the infinite recursion.

Alternative fix: recursion depth limit (weaker but common)

Add a hard cap:

if (++recursionDepth > 100) {
    error(errSyntaxError, -1, "Max object recursion exceeded");
    obj->initNull();
    return;
}

This prevents stack exhaustion but does not fully solve logical cycles

CVE-2019-13288 Details https://www.cvedetails.com/cve/CVE-2019-13288/

AFL++ GitHub Repository https://github.com/AFLplusplus/AFLplusplus

Fuzzing Security Vulnerabilities Codelabs https://fuzzing.in/codelabs/finding_security_vulnerabilities/

GDB PEDA / Pwndbg / GEF Collection https://github.com/apogiatzis/gdb-peda-pwndbg-gef

GEF (GDB Enhanced Features) Official Site https://hugsy.github.io/gef/

libexif

Original writeup on GitHub

Some things to make clear that no two fuzzing sessions will be the same because AFL is non deterministic.

This time, we turn our attention to fuzzing libexif, the widely used EXIF metadata parsing library, with the goal of stress-testing how it handles malformed and unexpected image metadata. Since libexif processes complex, attacker-controlled binary structures embedded inside image files, it presents an ideal surface for discovering memory safety issues such as out-of-bounds reads, integer overflows, and use-after-free bug

Lets set up our enviorment first

muffin@muffinn:/mnt/d$ cd $HOME
mkdir fuzzing_libexif && cd fuzzing_libexif/
muffin@muffinn:~/fuzzing_libexif$ wget https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gz
tar -xzvf libexif-0_6_14-release.tar.gz
--2026-01-11 05:40:23--  https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gz
Resolving github.com (github.com)... 20.207.73.82, 64:ff9b::14cf:4952
Connecting to github.com (github.com)|20.207.73.82|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://codeload.github.com/libexif/libexif/tar.gz/refs/tags/libexif-0_6_14-release [following]
--2026-01-11 05:40:24--  https://codeload.github.com/libexif/libexif/tar.gz/refs/tags/libexif-0_6_14-release
Resolving codeload.github.com (codeload.github.com)... 20.207.73.88, 64:ff9b::14cf:4958
Connecting to codeload.github.com (codeload.github.com)|20.207.73.88|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/x-gzip]
Saving to: ‘libexif-0_6_14-release.tar.gz’

libexif-0_6_14-release.tar.gz              [   <=>                                                                       ] 315.33K   641KB/s    in 0.5s

2026-01-11 05:40:26 (641 KB/s) - ‘libexif-0_6_14-release.tar.gz’ saved [322903]

image

Then lets build and test the application

./configure --disable-docs --prefix=$PWD/install
make
make install

image

It's only the docs that did'nt get built so lets move on


Since libexif is a library, we'll need another application that makes use of this library and which will be fuzzed. For this task we're going to use exif command-line.

I went with this ;

image

whats corpus ?

Corpus is basically a collection of inputs that the fuzzer will use as seeds . The fuzzer mutates these files to explore new code paths and trigger bugs.

We need a combination of good corpus and coverage to get the most out of fuzzing so a mix of seed corpus like

corpus/
├── img1.jpg
├── minimal_exif.jpg
├── truncated_ifd.jpg
└── weird_offsets.jpg

are good because they have a good validity to mutate accordingly , and get added back to the corpus again .

Let's get some inputs

cd $HOME/fuzzing_libexif
wget https://github.com/ianare/exif-samples/archive/refs/heads/master.zip
unzip master.zip

Yes and we can move onto AFL now

image

Some more things to know

Fuzzing source code is a three-step process:

  • Compile the target with a special compiler that prepares the target to be fuzzed efficiently. This step is called “instrumenting a target”.
  • Prepare the fuzzing by selecting and optimizing the input corpus for the target.
  • Perform the fuzzing of the target by randomly mutating input and assessing if that input was processed on a new path in the target binary.

AFL++ comes with a central compiler afl-cc that incorporates various different kinds of compiler targets and instrumentation options. The following evaluation flow will help you to select the best possible.

image

At a beginner level, you can think of fuzzing coverage as the fuzzer asking a very simple question over and over: “Did the program do something new this time?” Vanilla AFL answers this question using a small shared memory map and some random numbers that get baked into the binary during compilation. Each basic block in the program is given a random ID, and when the program jumps from one block to another, AFL combines the current and previous IDs and updates a slot in a 64 KB bitmap. You can roughly imagine the runtime logic as something like map[(cur_id ^ prev_id) & 0xFFFF]++. This works fine for small programs, but as soon as you fuzz a real target with thousands of blocks, many different edges end up writing to the same bitmap slot. From the fuzzer’s point of view, multiple distinct paths now look identical, so it stops rewarding inputs that actually explore new code.

You can see this behavior by building a target with classic AFL instrumentation. For example, compiling a program the old way looks like this:

export CC=afl-gcc
export CXX=afl-g++
./configure
make

At runtime, every execution updates the same fixed-size map, and collisions are inevitable. The fuzzer might mutate inputs for hours without realizing that it is technically reaching new logic, because the coverage feedback is too noisy. This is why fuzzing large libraries with vanilla AFL often plateaus early.

AFL++ fixes this by moving instrumentation to link time, using LLVM’s Link Time Optimization. Instead of assigning random IDs while compiling each source file independently, AFL++ waits until the linker has the full program in front of it. When you build in LTO mode, you typically do something like:

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

Under the hood, this causes all object files to be compiled into LLVM bitcode rather than final machine code. During the link step, AFL++ replaces the system linker with its own afl-ld. The LLVM linker then sees every function and every control-flow edge in the entire program and assigns a unique, deterministic ID to each edge. Because these IDs are planned globally, they do not collide in the coverage map. Internally, this is very similar to how LLVM’s own coverage works when you compile with -fsanitize=coverage=edge.

From the fuzzer’s perspective, this makes a huge difference. Now, when an input triggers a new path, AFL++ can clearly see a new edge being hit and will keep that input as interesting. You can even inspect the instrumentation being chosen by AFL++ at startup:

afl-fuzz -i corpus -o out -- ./target @@

You will see messages indicating that LTO-based, collision-free instrumentation is in use. The end result is that AFL++ gets a clean, high-resolution view of program behavior, allowing it to guide mutations far more intelligently. For a beginner, the takeaway is that vanilla AFL’s coverage is like a blurry heatmap where many paths overlap, while AFL++ with LTO gives you a sharp, accurate map of execution, which directly translates into better fuzzing results.

Let's build it using afl-lto this time

rm -r $HOME/fuzzing_libexif/install
cd $HOME/fuzzing_libexif/libexif-libexif-0_6_14-release/
make clean
export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/"
make
make install

And fuzz it

image

afl-fuzz -i $HOME/fuzzing_libexif/exif-samples-master/jpg/ -o $HOME/fuzzing_libexif/out/ -s 123 -- $HOME/fuzzing_libexif/install/bin/exif @@

We do get some crashes but instaed of using gef like last time , we'll use eclipse IDE

image

And open it

image

choose C/C++ -> "Existing code as makefile project". Then we need to select "Linux GCC"

image

And click the project explorer

image

Use the debug settings from here ,

image

We need to give the arguments of the crash to the debugger ;

muffin@muffinn:~/fuzzing_libexif/out/default/crashes$ ls
README.txt                                                                      id:000023,sig:11,src:000815,time:1969442,execs:1695366,op:havoc,rep:4
id:000000,sig:11,src:000002,time:5482,execs:5538,op:flip32,pos:707              id:000024,sig:11,src:000835,time:2113626,execs:1828903,op:havoc,rep:8
id:000001,sig:11,src:000002,time:5484,execs:5540,op:flip32,pos:719              id:000025,sig:11,src:000631,time:2208485,execs:1887093,op:havoc,rep:6
id:000002,sig:11,src:000002,time:11742,execs:11370,op:arith32,pos:34,val:-9     id:000026,sig:11,src:000872,time:2350026,execs:1976574,op:havoc,rep:7
id:000003,sig:11,src:000002,time:19506,execs:18791,op:havoc,rep:3               id:000027,sig:11,src:000872,time:2350454,execs:1976943,op:havoc,rep:2
id:000004,sig:11,src:000002,time:40736,execs:37405,op:havoc,rep:3               id:000028,sig:11,src:000871,time:2451781,execs:2050434,op:havoc,rep:1
id:000005,sig:11,src:000002,time:42758,execs:39273,op:havoc,rep:4               id:000029,sig:11,src:000781,time:2496223,execs:2079732,op:havoc,rep:11
id:000006,sig:11,src:000002,time:44412,execs:40654,op:havoc,rep:7               id:000030,sig:11,src:000897,time:2700414,execs:2216694,op:havoc,rep:6
id:000007,sig:11,src:000005,time:79592,execs:71711,op:arith32,pos:34,val:be:-9  id:000031,sig:11,src:000900,time:2770720,execs:2263746,op:havoc,rep:3
id:000008,sig:11,src:000007,time:138315,execs:127188,op:inf,rep:1               id:000032,sig:11,src:000918,time:3069941,execs:2501103,op:havoc,rep:9
id:000009,sig:11,src:000008,time:145380,execs:133020,op:flip32,pos:1141         id:000033,sig:11,src:000557,time:3305209,execs:2720118,op:havoc,rep:7
id:000010,sig:11,src:000022,time:153288,execs:139444,op:havoc,rep:8             id:000034,sig:11,src:000029,time:3995534,execs:3335299,op:havoc,rep:15
id:000011,sig:11,src:000042,time:167548,execs:151848,op:havoc,rep:3             id:000035,sig:11,src:000967,time:4009409,execs:3348305,op:havoc,rep:2
id:000012,sig:11,src:000085,time:191380,execs:171815,op:havoc,rep:7             id:000036,sig:11,src:000983,time:4382205,execs:3640984,op:havoc,rep:3
id:000013,sig:11,src:000085,time:204360,execs:181159,op:havoc,rep:14            id:000037,sig:11,src:000987,time:4466462,execs:3705557,op:havoc,rep:13
id:000014,sig:11,src:000497,time:614590,execs:532743,op:havoc,rep:1             id:000038,sig:11,src:000818,time:5235372,execs:4354123,op:havoc,rep:6
id:000015,sig:11,src:000281,time:1284590,execs:1157542,op:havoc,rep:9           id:000039,sig:11,src:001002,time:5696313,execs:4765548,op:havoc,rep:5
id:000016,sig:11,src:000688,time:1292284,execs:1165404,op:havoc,rep:7           id:000040,sig:11,src:000999,time:5805201,execs:4859084,op:havoc,rep:1
id:000017,sig:11,src:000688,time:1295490,execs:1169759,op:havoc,rep:11          id:000041,sig:11,src:000061,time:5978992,execs:5006835,op:havoc,rep:5
id:000018,sig:11,src:000721,time:1323786,execs:1200738,op:havoc,rep:6           id:000042,sig:11,src:000790,time:6020901,execs:5043916,op:havoc,rep:4
id:000019,sig:11,src:000746,time:1410128,execs:1283634,op:havoc,rep:16          id:000043,sig:11,src:000081,time:6545331,execs:5287820,op:havoc,rep:2
id:000020,sig:11,src:000552,time:1573330,execs:1402347,op:havoc,rep:16          id:000044,sig:11,src:000028,time:6713517,execs:5401827,op:havoc,rep:8
id:000021,sig:11,src:000552,time:1573386,execs:1402379,op:havoc,rep:5           id:000045,sig:11,src:000775,time:7532084,execs:6099251,op:havoc,rep:3
id:000022,sig:11,src:000630,time:1815249,execs:1567843,op:havoc,rep:1

image

And we hit a breakpoint so let's try for a segfault

image

And we get one

image

Let's verify the crash with gef

muffin@muffinn:~/fuzzing_libexif/exif-exif-0_6_15-release$ gdb ~/fuzzing_libexif/install/bin/exif
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007ffef7c00010  →  0x00000000007fff00
$rbx   : 0x0
$rcx   : 0x15
$rdx   : 0x3ddc
$rsp   : 0x00007fffffffd7a8  →  0x00005555555832b5  →  <exif_mnote_data_canon_load+02c5> mov edx, DWORD PTR [rsp+0x1c]
$rbp   : 0x1
$rsi   : 0x00005555557d1f89  →  0x0000000000000000
$rdi   : 0x00007ffef7c185c0  →  0x0000000000000000
$rip   : 0x00007ffff7d88fe8  →  <__memmove_avx_unaligned_erms+05a8> vmovdqu ymm15, YMMWORD PTR [rsi+0x3060]
$r8    : 0xffffffffffffffd0
$r9    : 0x00005555555be73f  →  0x0000000000000001
$r10   : 0x3fff9
$r11   : 0x6a00000
$r12   : 0x00005555557bb690  →  0x00005555557bb720  →  0x0000000000000001
$r13   : 0x00005555557b9620  →  0x4949000066697845 ("Exif"?)
$r14   : 0x00005555555b2930  →  0x0000000000000000
$r15   : 0xffffffffffffff90
$eflags: [zero CARRY parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd7a8│+0x0000: 0x00005555555832b5  →  <exif_mnote_data_canon_load+02c5> mov edx, DWORD PTR [rsp+0x1c]       ← $rsp
0x00007fffffffd7b0│+0x0008: 0x00005555557b9620  →  0x4949000066697845 ("Exif"?)
0x00007fffffffd7b8│+0x0010: 0x00000cad557bb690
0x00007fffffffd7c0│+0x0018: 0x00005555557b8a50  →  0x0000000300000000
0x00007fffffffd7c8│+0x0020: 0x0000000e000003b9
0x00007fffffffd7d0│+0x0028: 0x0000555555582000  →  <exif_mnote_data_canon_count+0000> endbr64
0x00007fffffffd7d8│+0x0030: 0x00005555555b2930  →  0x0000000000000000
0x00007fffffffd7e0│+0x0038: 0xffffffffffffff90
──────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7d88fd6 <__memmove_avx_unaligned_erms+0596> add    BYTE PTR [rax], al
   0x7ffff7d88fd8 <__memmove_avx_unaligned_erms+0598> vmovdqu ymm13, YMMWORD PTR [rsi+0x3020]
   0x7ffff7d88fe0 <__memmove_avx_unaligned_erms+05a0> vmovdqu ymm14, YMMWORD PTR [rsi+0x3040]
 → 0x7ffff7d88fe8 <__memmove_avx_unaligned_erms+05a8> vmovdqu ymm15, YMMWORD PTR [rsi+0x3060]
   0x7ffff7d88ff0 <__memmove_avx_unaligned_erms+05b0> sub    rsi, 0xffffffffffffff80
   0x7ffff7d88ff4 <__memmove_avx_unaligned_erms+05b4> vmovntdq YMMWORD PTR [rdi], ymm0
   0x7ffff7d88ff8 <__memmove_avx_unaligned_erms+05b8> vmovntdq YMMWORD PTR [rdi+0x20], ymm1
   0x7ffff7d88ffd <__memmove_avx_unaligned_erms+05bd> vmovntdq YMMWORD PTR [rdi+0x40], ymm2
   0x7ffff7d89002 <__memmove_avx_unaligned_erms+05c2> vmovntdq YMMWORD PTR [rdi+0x60], ymm3
──────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "exif", stopped 0x7ffff7d88fe8 in __memcpy_avx_unaligned_erms (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7d88fe8 → __memcpy_avx_unaligned_erms()
[#1] 0x5555555832b5 → memcpy(__len=<optimized out>, __src=<optimized out>, __dest=<optimized out>)
[#2] 0x5555555832b5 → exif_mnote_data_canon_load(ne=0x5555557bb690, buf=<optimized out>, buf_size=<optimized out>)
[#3] 0x5555555705c9 → exif_data_load_data(data=0x5555557b8610, d_orig=<optimized out>, ds_orig=<optimized out>)
[#4] 0x55555557d67d → exif_loader_get_data(loader=0x5555557b85c0)
[#5] 0x55555555f3af → main(argc=<optimized out>, argv=<optimized out>)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
gef➤  bt
#0  0x00007ffff7d88fe8 in __memcpy_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:833
#1  0x00005555555832b5 in memcpy (__len=<optimized out>, __src=<optimized out>, __dest=<optimized out>)
    at /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#2  exif_mnote_data_canon_load (ne=0x5555557bb690, buf=<optimized out>, buf_size=<optimized out>)
    at exif-mnote-data-canon.c:224
#3  0x00005555555705c9 in exif_data_load_data (data=data@entry=0x5555557b8610, d_orig=<optimized out>,
    ds_orig=<optimized out>) at exif-data.c:867
#4  0x000055555557d67d in exif_loader_get_data (loader=loader@entry=0x5555557b85c0) at exif-loader.c:387
#5  0x000055555555f3af in main (argc=<optimized out>, argv=<optimized out>) at main.c:438

The crash occurs inside memcpy, which immediately suggests a memory safety violation. Since memcpy performs no bounds checking, a segmentation fault here usually indicates that either the source pointer, destination pointer, or length argument is invalid. In fuzzing contexts, this almost always points to attacker-controlled size or offset fields being trusted without sufficient validation.

The most important frame is frame #2: exif_mnote_data_canon_load at line 224. This function is responsible for parsing Canon-specific MakerNote EXIF data. MakerNotes are vendor-specific metadata blocks embedded inside EXIF, and they are historically one of the most error-prone parts of image parsers due to their lack of strict standardization. Seeing a crash here is a strong signal that malformed MakerNote data is triggering unsafe memory operations.

The call stack also shows how execution reached this point. The malformed input is processed by exif_data_load_data, which is the main EXIF parsing entry point. From there, control flows through the EXIF loader and eventually into the Canon MakerNote parser. This confirms that the crash is not caused by command-line argument handling or CLI glue code, but by core libexif parsing logic.

Given that the fault occurs during a memcpy inside the MakerNote parser, the most likely root cause is an out-of-bounds read or write. This typically happens when length fields embedded in the EXIF data are used directly to determine how many bytes to copy, without checking whether those bytes actually exist within the input buffer. Depending on whether the invalid access occurs on the source or destination side of memcpy, this can result in an out-of-bounds read or an out-of-bounds write. Either case represents a serious memory safety issue

References

Fuzzing Report

Original writeup on GitHub

Build Configuration

The target is compiled using AFL’s GCC wrapper with sanitizers and debug symbols enabled.

AFL build output

Fuzzing Invocation

The fuzzer is launched with a minimal seed corpus. AFL detects available CPU cores and binds to a free core automatically.

AFL startup and CPU binding

Fuzzing Behavior

As malformed inputs are generated, the program consistently crashes, with AFL identifying multiple unique crash conditions while producing thousands of total crash instances through continued mutation.

AFL fuzzing results and crashes

Crash Detection

The reported crashes are detected by sanitizers.

image