# LibAFL: a Rust fuzzing library you assemble yourself

> LibAFL is not a fuzzer you download and point at a binary. It is a Rust library of fuzzing components you wire together, and that distinction decides whether it fits your project.

**AFLplusplus/LibAFL** — Advanced Fuzzing Library - Slot your Fuzzer together in Rust! Scales across cores and machines. For Windows, Android, MacOS, Linux, no_std, ...

- Repository: https://github.com/AFLplusplus/LibAFL
- Stars: 2,645 · Forks: 485
- Language: Rust
- License: NOASSERTION
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/aflplusplus-libafl

## What LibAFL solves that a finished fuzzer does not

Off-the-shelf fuzzers make one set of decisions for you: how input is represented, how coverage is measured, how the corpus is scheduled, how crashes are detected. Those decisions are usually right. When they are wrong, you have two options. Patch the fuzzer, which means fighting a codebase you did not write, or write your own fuzzer from scratch, which means reimplementing a corpus, a mutator, a feedback loop and a crash handler before you test anything.

LibAFL is the third option. The README describes it as "a collection of reusable pieces of fuzzers, written in Rust" that gives "many of the benefits of an off-the-shelf fuzzer, while being completely customizable." The intended reader is not someone who wants to fuzz a parser for an afternoon. It is someone who needs an AST-based input instead of a byte array, or a custom coverage signal, or a fuzzer small enough to fit on an embedded device.

The project is a Cargo workspace, and the layout reflects that split. The main crate is crates/libafl, the instrumentation helpers live in crates/libafl_targets, and compiler wrapping lives in crates/libafl_cc. The workspace members list also includes crates/libafl_frida, crates/libafl_qemu, crates/libafl_tinyinst, crates/libafl_nyx, crates/libafl_unicorn and crates/libafl_intelpt, so each instrumentation backend is a separate crate you opt into rather than a dependency you carry by default.

## The parts you slot together: input, observer, feedback, stage

The README's own framing is "Slot your own fuzzers together and extend their features using Rust." Concretely, a LibAFL fuzzer is a set of independent pieces with defined interfaces. An input type describes what the fuzzer mutates, and the README notes that BytesInput "is just one potential form input" and invites an AST-based replacement for structured fuzzing. An observer watches the target and records something, typically coverage. Feedback turns what the observer recorded into a decision about whether a test case is interesting. A stage performs a mutation strategy on the queue entry.

Because these are separate pieces, replacing one does not force you to replace the others. That is the whole design bet, and it is also where the cost sits: the library hands you the interfaces and expects you to choose implementations and connect them.

Scaling is handled by LLMP, which the README expands as Low Level Message Passing. It is described as allowing LibAFL to "scale almost linearly over cores, and via TCP to multiple machines." For the multi-machine path, the workspace has a separate excluded utility, utils/multi_machine_generator, which suggests the cross-machine setup is generated rather than hand-written.

On instrumentation, the supported backends are SanitizerCoverage via libafl_targets, Frida via libafl_frida, QEMU in both user-mode and system mode with emulation hooks via libafl_qemu, and TinyInst via libafl_tinyinst. The README states that custom instrumentation backends are easy to add. The performance claim in the README is specific and attributed to users rather than to a benchmark run: "Users reach 120k execs/sec in frida-mode on a phone (using all cores)." That is a user-reported figure, not a measurement you can verify from the repository.

## Installing LibAFL and running an example fuzzer

The README is explicit that the Rust toolchain should come from rustup rather than a Linux distribution package, which it calls "likely outdated." The minimum supported Rust version is pinned in crates/libafl/Cargo.toml; the workspace manifest in the repository root sets rust-version to 1.93.1. If your installed compiler is older than the pinned value, the README gives one command:

```bash
rustup update stable
```

LLVM tools including clang and clang++ are required, and the README states the supported range as newer than LLVM 15.0.0 up to LLVM 18.1.3. On Debian or Ubuntu it points to the packages at apt.llvm.org instead of the distribution's own. You also need just, the command runner used to build the fuzzers under fuzzers/. The Dockerfile installs it with cargo binstall --no-confirm just, and installs cargo-nextest, cargo-fuzz and taplo-cli the same way.

With those in place, clone and build:

```bash
git clone https://github.com/AFLplusplus/LibAFL
cd LibAFL
cargo build --release
```

The build produces the library and its workspace members. The README also documents cargo doc for API documentation and, for the book, cd docs && mdbook serve, which requires mdbook to be installed separately.

The first real use is not a library call. It is running an example fuzzer, because the README says the examples are "the natural way to get started" and points at ./fuzzers. Each example directory that contains a Justfile can be run through the runner:

```bash
just run
```

The best-tested example, per the README, is ./fuzzers/inprocess/libfuzzer_libpng, described as a multicore libfuzzer-like fuzzer using LibAFL for a libpng harness. Read that example's source before writing your own; it shows how the input, observer, feedback and stages are wired for a real target.

## Where LibAFL is the wrong tool

The cost is real and the README does not hide it. You are building a fuzzer, not configuring one. If your target is a C library with a straightforward entry point, a stock coverage-guided fuzzer will find crashes this week while you are still deciding which observer to use. The library's value shows up when the default decisions are wrong for your target, and it is negative when they are right.

The no_std mode is the clearest example of a trade-off rather than a feature. The README says LibAFL "can be built in no_std mode to inject LibAFL into obscure targets like embedded devices and hypervisors." That capability comes with a constrained environment, and the workspace lists crates/no_std_time as a separate member, which tells you the standard library's time facilities are not simply available in that configuration.

The book is marked WIP in the README, twice. The API documentation at docs.rs is the more reliable reference, and the README leans on external material: a CCS 2022 paper, an RC3 talk, a Fuzzcon Europe talk described as "a bit but not so much outdated," Fuzzing101 solutions, and a workshop by Atredis. That is a lot of learning surface for one library, and it means the documentation burden lands partly on you.

There is also a versioning signal worth reading plainly. The last push to the repository was on 2026-09-16, and the most recent release listed is 0.16.1 on 2026-08-11, preceded by 0.16.0 on 2026-08-10. The jump from 0.15.4 in November 2025 to 0.16.0 in August 2026 is a major-version boundary in a pre-1.0 project, and the repository carries a MIGRATION.md at the top level. Expect to read it when you upgrade.

## LibAFL against libFuzzer and AFL++

The honest comparison is not LibAFL versus another library. It is LibAFL versus not writing a fuzzer at all.

libFuzzer is a fuzzing engine you link into a target through a single entry point. You write LLVMFuzzerTestOneInput, compile with the sanitizer coverage flags, and the engine owns the corpus, the mutations and the crash handling. LibAFL inverts that: the corpus, the mutations and the crash handling are yours to select, and the library supplies the parts. The repository acknowledges the relationship directly, since crates/libafl_libfuzzer exists as a workspace member and fuzzers/inprocess/libfuzzer_libpng is described as a libfuzzer-like fuzzer built on LibAFL. If your goal is a libFuzzer-shaped fuzzer without libFuzzer's fixed decisions, that example is the starting point.

AFL++ is a complete fuzzer with its own build wrapper and a long history of binary-only modes. LibAFL comes from the same organization, and the README's own comparison to AFL++ is implicit in what it offers instead: a Rust library where each part is replaceable, running on Windows, macOS, iOS, Linux and Android, and buildable in no_std mode. Choose AFL++ when you want a working fuzzer today. Choose LibAFL when you need a fuzzer that does something AFL++ does not, and you are willing to build it.

## Licence, upgrades and what they cost you

The repository root holds LICENSE-APACHE and LICENSE-MIT, and the README states the project is "Licensed under either of Apache License, Version 2.0 or MIT license at your option." The workspace manifest agrees, setting license to "MIT OR Apache-2.0". That dual arrangement is the common Rust convention and is generally permissive, but the repository metadata carries NOASSERTION as the detected licence, so a compliance check should read the two licence files rather than the metadata field. This is not legal advice; if your organization has a policy on dual-licensed dependencies, route it through the people who own that policy.

Upgrade cost has two components. The first is the pre-1.0 version line: 0.16.0 and 0.16.1 arrived within a day of each other in August 2026, after a nine-month gap from 0.15.4. The presence of MIGRATION.md at the repository root is the project's own acknowledgement that upgrades need instructions. The second is the pinned toolchain. The workspace sets rust-version to 1.93.1, and the Dockerfile builds on rust:1.91.0, so a container-based workflow and a host toolchain can drift apart. Before you pin LibAFL in a long-lived project, decide which of those two numbers your CI follows.

Instrumentation backends add their own upkeep. libafl_qemu, libafl_frida, libafl_tinyinst and libafl_unicorn track external projects with their own release cycles, and the README's LLVM range of newer than 15.0.0 up to 18.1.3 is a constraint you inherit.

## Conclusion

Adopt LibAFL if you need a fuzzer whose input type, feedback and scheduling you control, or if you are fuzzing a target that no off-the-shelf fuzzer reaches, such as an embedded device, a hypervisor or a binary-only phone app. Do not adopt it if a stock fuzzer already covers your target: you will spend days on harness plumbing before the first crash. Before committing, check the rust-version key in crates/libafl/Cargo.toml against your toolchain, confirm your LLVM version sits inside the supported range, and build the inprocess libfuzzer_libpng example to see the wiring in a working state.

## FAQ

### How do you use LibAFL?

Clone the repository, install Rust through rustup and the LLVM tools, then build with cargo build --release. From there the README directs you to the examples under fuzzers/, run through just run, with fuzzers/inprocess/libfuzzer_libpng described as the best-tested starting point.

### What is the difference between LibAFL and libFuzzer?

libFuzzer is a complete engine you link into a target through one entry point, while LibAFL is a collection of reusable fuzzing components whose input type, observer, feedback and stages you choose and connect yourself. The repository includes a libfuzzer-like example in fuzzers/inprocess/libfuzzer_libpng and a libafl_libfuzzer crate, so the two approaches overlap by design.

### Which instrumentation backends does LibAFL support?

The README lists SanitizerCoverage in libafl_targets, Frida in libafl_frida, QEMU user-mode and system mode with emulation hooks in libafl_qemu, and TinyInst in libafl_tinyinst. It also states that custom instrumentation backends are easy to add.

### Which Rust and LLVM versions does LibAFL require?

The README says to install Rust through rustup rather than a distribution package, and the workspace manifest pins rust-version to 1.93.1. LLVM tools including clang and clang++ must be newer than LLVM 15.0.0 up to LLVM 18.1.3.

### Does LibAFL run on Windows, macOS and Android?

The README states that LibAFL runs on Windows, macOS, iOS, Linux and Android, and that it can be built in no_std mode for targets such as embedded devices and hypervisors. The Dockerfile additionally adds cross-compilation targets for armv7, aarch64, i686 and powerpc.

## Sources

- [AFLplusplus/LibAFL on GitHub](https://github.com/AFLplusplus/LibAFL)
- [Issues](https://github.com/AFLplusplus/LibAFL/issues)
- [README](https://github.com/AFLplusplus/LibAFL/blob/main/README.md)
- [Releases](https://github.com/AFLplusplus/LibAFL/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aflplusplus-libafl
