Introduction
Fuzz testing is a software testing technique used to find security and stability issues by providing pseudo-random data as input to the software.
Rust is a high performance, safe, general purpose programming language.
This book demonstrates how to perform fuzz testing for software written in Rust.
There are two tools for fuzzing Rust code documented in this book: afl.rs and cargo-fuzz.
The source of this book is available on GitHub at https://github.com/rust-fuzz/book.
Fuzzing with cargo-fuzz
cargo-fuzz is the recommended tool for fuzz testing Rust code.
cargo-fuzz is itself not a fuzzer, but a tool to invoke a fuzzer. Currently, the only fuzzer it supports is libFuzzer (through the libfuzzer-sys crate), but it could be extended to support other fuzzers in the future.
Setup
Requirements
libFuzzer needs LLVM sanitizer support; this works on x86-64 Linux, x86-64 macOS and Apple-Silicon (aarch64) macOS, and Windows (thanks to the MSVC AddressSanitizer). Requires a C++ compiler with C++11 support. Rust provides multiple compilers. This project requires the nightly compiler since it uses the -Z
compiler flag to provide address sanitization. Assuming you used rustup to install Rust, you can check your default compiler with:
$ rustup default
stable-x86_64-unknown-linux-gnu (default) # Not the compiler we want.
To change to the nightly compiler:
$ rustup install nightly
$ rustup default nightly
nightly-x86_64-unknown-linux-gnu (default) # The correct compiler.
Installing
cargo install cargo-fuzz
Upgrading
cargo install --force cargo-fuzz
Tutorial
For this tutorial, we're going to be fuzzing the URL parsing crate rust-url. Our goal here is to find some input generated by the fuzzer such that, when passed to Url::parse
, it causes some sort of panic or crash to happen.
To start, clone the rust-url repository and change directories into it:
git clone https://github.com/servo/rust-url.git
cd rust-url
Although we could fuzz the latest commit on master
, we're going to checkout a specific revision that is known to have a parsing bug:
git checkout bfa167b4e0253642b6766a7aa74a99df60a94048
Initialize cargo-fuzz:
cargo fuzz init
This will create a directory called fuzz_targets
which will contain a collection of fuzzing targets. It is generally a good idea to check in the files generated by init
. Each fuzz target is a Rust program that is given random data and tests a crate (in this case, rust-url). cargo fuzz init
automatically generates an initial fuzz target for us.
Use cargo fuzz list
to view the list of all existing fuzz targets:
cargo fuzz list
The source code for this fuzz target by default lives in fuzz/fuzz_targets/<fuzz target name>.rs
. Open that file and edit it to look like this:
#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate url;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = url::Url::parse(s);
}
});
libFuzzer
is going to repeatedly call the body of fuzz_target!()
with a slice of pseudo-random bytes, until your program hits an error condition (segfault, panic, etc). Write your fuzz_target!()
body to hit the entry point you need.
Since the generated data is a byte slice, we'll need to convert it to a UTF-8 &str
since rust-url expects that when parsing.
To begin fuzzing, run:
cargo fuzz run <fuzz target name>
Congratulations, you're fuzzing! The output you're seeing is generated by the fuzzer libFuzzer. To learn more about what the output means see the 'output' section in the libFuzzer documentation.
If you leave it going for long enough you'll eventually discover a crash. The output would look something like this:
...
#56232 NEW cov: 2066 corp: 110/4713b exec/s: 11246 rss: 170Mb L: 42 MS: 1 EraseBytes-
#58397 NEW cov: 2069 corp: 111/4755b exec/s: 11679 rss: 176Mb L: 42 MS: 1 EraseBytes-
#59235 NEW cov: 2072 corp: 112/4843b exec/s: 11847 rss: 178Mb L: 88 MS: 4 InsertByte-ChangeBit-CopyPart-CopyPart-
#60882 NEW cov: 2075 corp: 113/4953b exec/s: 12176 rss: 183Mb L: 110 MS: 1 InsertRepeatedBytes-
thread '<unnamed>' panicked at 'index out of bounds: the len is 1 but the index is 1', src/host.rs:105
note: Run with `RUST_BACKTRACE=1` for a backtrace.
==70997== ERROR: libFuzzer: deadly signal
#0 0x1097c5500 in __sanitizer_print_stack_trace (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x62500)
#1 0x108383d1b in fuzzer::Fuzzer::CrashCallback() (fuzzer_script_1:x86_64+0x10002fd1b)
#2 0x108383ccd in fuzzer::Fuzzer::StaticCrashSignalCallback() (fuzzer_script_1:x86_64+0x10002fccd)
#3 0x1083d19c7 in fuzzer::CrashHandler(int, __siginfo*, void*) (fuzzer_script_1:x86_64+0x10007d9c7)
...
#33 0x10838b393 in fuzzer::Fuzzer::Loop() (fuzzer_script_1:x86_64+0x100037393)
#34 0x1083650ec in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (fuzzer_script_1:x86_64+0x1000110ec)
#35 0x108396c3f in main (fuzzer_script_1:x86_64+0x100042c3f)
#36 0x7fff91552234 in start (libdyld.dylib:x86_64+0x5234)
NOTE: libFuzzer has rudimentary signal handlers.
Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 2 InsertByte-EraseBytes-; base unit: 3c4fc9770beb5a732d1b78f38cc8b62b20cb997c
0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x5b,0x3a,0x5d,0x3a,0x78,0xc5,0xa4,0x1,0x3a,0x7f,0x1,0x59,0xc5,0xa4,0xd,0x78,0x78,0x3a,0x78,0x69,0x3a,0x0,0x69,0x3a,0x5c,0xd,0x7e,0x78,0x40,0x0,0x25,0xa,0x0,0x29,0x20,
http://[:]:x\xc5\xa4\x01:\x7f\x01Y\xc5\xa4\x0dxx:xi:\x00i:\\\x0d~x@\x00%\x0a\x00)
artifact_prefix='/private/tmp/rust-url/fuzz/artifacts/fuzzer_script_1/'; Test unit written to /home/user/rust-url/fuzz/artifacts/fuzzer_script_1/crash-e9b1b5183e46a288c25a2a073262cdf35408f697
Base64: aHR0cDovL1s6XTp4xaQBOn8BWcWkDXh4OnhpOgBpOlwNfnhAACUKACkg
The line in the output that starts with http
is the input that causes a panic in rust-url.
Guide
All available commands available for cargo-fuzz:
cargo fuzz --help
Run a target:
cargo fuzz run <fuzz target name>
Cargo features
It is possible to fuzz crates with different configurations of Cargo features by using the command line options --features
, --no-default-features
and --all-features
. Note that these options control the fuzz_targets
crate; you will need to forward them to the crate being fuzzed by e.g. adding the following to fuzz_targets/Cargo.toml
:
[features]
unsafe = ["project/unsafe"]
#[cfg(fuzzing)]
Every crate instrumented for fuzzing -- the fuzz_targets
crate, the project crate, and their entire dependency tree -- is compiled with the --cfg fuzzing
rustc option. This makes it possible to disable code paths that prevent fuzzing from working, e.g. verification of cryptographic signatures, with a simple #[cfg(not(fuzzing))]
, and without the need for an externally visible Cargo feature that must be maintained throughout every dependency.
libFuzzer configuration options
See all the libFuzzer options:
cargo fuzz run <fuzz target name> -- -help=1
For example, to generate only ASCII inputs, run:
cargo fuzz run <fuzz target name> -- -only_ascii=1
Structure-Aware Fuzzing
Not every fuzz target wants to take a buffer of raw bytes as input. We might
want to only feed it well-formed instances of some structured data. Luckily, the
libfuzzer-sys
crate enables us to define fuzz targets that take any kind of type,
as long as it implements the Arbitrary
trait.
libfuzzer_sys::fuzz_target!(|input: AnyTypeThatImplementsArbitrary| {
// Use `input` here...
})
The arbitrary
crate implements Arbitrary
for nearly all the types in std
,
including collections like Vec
and HashMap
as well as things like String
and PathBuf
.
For convenience, the libfuzzer-sys
crate re-exports the arbitrary
crate as
libfuzzer_sys::arbitrary
. You can also enable #[derive(Arbitrary)]
either by
- enabling the
arbitary
crate's"derive"
feature, or - (equivalently) enabling the
libfuzzer-sys
crate's"arbitrary-derive"
feature.
See the arbitrary
crate's documentation for more details.
This section concludes with two examples of structure-aware fuzzing:
Example 1: Fuzzing Color Conversions
Let's say we are working on a color conversion library that can turn RGB colors into HSL and back again.
Enable Deriving Arbitrary
We are lazy, and don't want to implement Arbitrary
by hand, so we want to
enable the arbitrary
crate's "derive"
cargo feature. This lets us get
automatic Arbitrary
implementations with #[derive(Arbitrary)]
.
Because the Rgb
type we will be deriving Arbitrary
for is in our main color
conversion crate, we add this to our main Cargo.toml
.
# Cargo.toml
[dependencies]
arbitrary = { version = "1", optional = true, features = ["derive"] }
Derive Arbitrary
for our Rgb
Type
In our main crate, when the "arbitrary"
cargo feature is enabled, we derive
the Arbitrary
trait:
// src/lib.rs
#[derive(Clone, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
Enable the Main Project's "arbitrary"
Cargo Feature for the Fuzz Targets
Because we made arbitrary
an optional dependency in our main color conversion
crate, we need to enable that feature for our fuzz targets to use it.
# fuzz/Cargo.toml
[dependencies]
my_color_conversion_library = { path = "..", features = ["arbitrary"] }
Add the Fuzz Target
We need to add a new fuzz target to our project:
$ cargo fuzz add rgb_to_hsl_and_back
Implement the Fuzz Target
Finally, we can implement our fuzz target that takes arbitrary RGB colors,
converts them to HSL, and then converts them back to RGB and asserts that we get
the same color as the original! Because we implement Arbitrary
for our Rgb
type, our fuzz target can take instances of Rgb
directly:
// fuzz/fuzz_targets/rgb_to_hsl_and_back.rs
libfuzzer_sys::fuzz_target!(|color: Rgb| {
let hsl = color.to_hsl();
let rgb = hsl.to_rgb();
// This should be true for all RGB -> HSL -> RGB conversions!
assert_eq!(color, rgb);
});
Example 2: Fuzzing Allocator API Calls
Imagine, for example, that we are fuzzing our own malloc
and free
implementation. We want to make a sequence of valid allocation and deallocation
API calls. Additionally, we want that sequence to be guided by the fuzzer, so it
can use its insight into code coverage to maximize the amount of code we
exercise during fuzzing.
Add the Fuzz Target
First, we add a new fuzz target to our project:
$ cargo fuzz add fuzz_malloc_free
Enable Deriving Arbitrary
Like the color conversion example above, we don't want to write our Arbitrary
implementation by hand, we want to derive it.
# fuzz/Cargo.toml
[dependencies]
libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] }
Define an AllocatorMethod
Type and Derive Arbitrary
Next, we define an enum
that represents either a malloc
, a realloc
, or a
free
:
// fuzz_targets/fuzz_malloc_free.rs
use libfuzzer_sys::arbitrary::Arbitrary;
#[derive(Arbitrary, Debug)]
enum AllocatorMethod {
Malloc {
// The size of allocation to make.
size: usize,
},
Free {
// Free the index^th allocation we've made.
index: usize
},
Realloc {
// We will realloc the index^th allocation we've made.
index: usize,
// The new size of the allocation.
new_size: usize,
},
}
Write a Fuzz Target That Takes a Sequence of AllocatorMethod
s
Finally, we write a fuzz target that takes a vector of AllocatorMethod
s and
interprets them by making the corresponding malloc
, realloc
, and free
calls. This works because Vec<T>
implements Arbitrary
when T
implements
Arbitrary
.
// fuzz/fuzz_targets/fuzz_malloc_free.rs
libfuzzer_sys::fuzz_target!(|methods: Vec<AllocatorMethod>| {
let mut allocs = vec![];
// Interpret the fuzzer-provided methods and make the
// corresponding allocator API calls.
for method in methods {
match method {
AllocatorMethod::Malloc { size } => {
let ptr = my_allocator::malloc(size);
allocs.push(ptr);
}
AllocatorMethod::Free { index } => {
match allocs.get(index) {
Some(ptr) if !ptr.is_null() => {
my_allocator::free(ptr);
allocs[index] = std::ptr::null();
}
_ => {}
}
}
AllocatorMethod::Realloc { index, size } => {
match allocs.get(index) {
Some(ptr) if !ptr.is_null() => {
let new_ptr = my_allocator::realloc(ptr, size);
allocs[index] = new_ptr;
}
_ => {}
}
}
}
}
// Free any remaining allocations.
for ptr in allocs {
if !ptr.is_null() => {
my_allocator::free(ptr);
}
}
});
Code Coverage
Visualizing code coverage helps you understand which code paths are being fuzzed and — more importantly — which aren't. To help the fuzzer exercise new code paths, you can look at what it is failing to reach and then either add new seed inputs to the corpus, or tweak the fuzz target. This chapter describes how to generate coverage reports for your fuzz target and its current corpus.
Prerequisites
First, install the LLVM-coverage tools as described in the rustc book.
If you are using a non-nightly toolchain as your default toolchain, remember to
install the rustup components for the nightly toolchain instead of the default
(rustup component add --toolchain nightly llvm-tools-preview ...
).
You must also have cargo fuzz
version 0.10.0
or newer to use the cargo fuzz coverage
subcommand.
Generate Code-Coverage Data
After you fuzzed your program, use the coverage
command to generate precise
source-based code coverage information:
$ cargo fuzz coverage <target> [corpus dirs] [-- <args>]
This command
-
compiles your project using the
-Cinstrument-coverage
Rust compiler flag, -
runs the program without fuzzing on the provided corpus (if no corpus directory is provided it uses
fuzz/corpus/<target>
by default), -
for each input file in the corpus, generates raw coverage data in the
fuzz/coverage/<target>/raw
subdirectory, and -
merges the raw files into a
coverage.profdata
file located in thefuzz/coverage/<target>
subdirectory.
Afterwards, you can use the generated coverage.profdata
file to generate
coverage reports and visualize code-coverage information as described in the
rustc book.
Example
Suppose we have a my_compiler
fuzz target for which we want to visualize code
coverage.
-
Run the fuzzer on the
my_compiler
target:$ cargo fuzz run my_compiler
-
Produce code-coverage information:
$ cargo fuzz coverage my_compiler
-
Visualize the coverage data in HTML:
$ cargo cov -- show fuzz/target/<target triple>/release/my_compiler \ --format=html \ -instr-profile=fuzz/coverage/my_compiler/coverage.profdata \ > index.html
There are many visualization and coverage-report options available (see
llvm-cov show --help
).
Targets
A collection of community maintained cargo-fuzz compatible fuzz targets can be found here.
Fuzzing on Windows
cargo-fuzz can be used to fuzz Windows programs, thanks to the MSVC AddressSanitizer. Read on to learn how to set up your Windows and PowerShell environment for building and fuzzing on Windows.
Windows Setup
It's possible to use cargo-fuzz to fuzz Rust code on Windows. This guide aims to shed some light on how you can get cargo-fuzz up and running on a Windows system.
1. Install Visual Studio
Make sure you have Visual Studio installed; there are a number of features that need to be installed alongside it in order for the fuzzing code to build. Follow this guide to install the "Visual Studio Installer". Use it to make sure you have the following individual components installed:
- MSVC v143 - VS 2022 C++ x64/x86 build tools
- (This was the latest at the time of writing - you may install a newer version if you're reading this at a later point in time!)
- C++ AddressSanitizer
2. Set up PowerShell
Certain directories must be on the PowerShell system $env:PATH
in order for
builds to succeed and for certain cargo-fuzz commands to work. The Developer PowerShell for VS 2022
and/or x64 Native Tools Command Prompt for VS 2022
may have these directories already on the path. If they don't, or you are using
a different PowerShell, make sure these directories are added to the shell's
$env:PATH
:
C:\Program Files\Microsoft Visual Studio\Community\VC\Tools\MSVC\<VERSION_NUMBER>\bin\Hostx86\64
- Where
<VERSION_NUMBER>
is the MSVC version you have installed.
- Where
- (Optional)
C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
- Add this if you want to use the Windows Debugger to debug your fuzzing targets.
These paths may very slightly on your machine, but the main idea is that your shell needs to be able to find the MSVC-based AddressSanitizer DLL as well as the other MSVC-related binaries.
Fuzzing Windows DLLs
On Windows systems, shared libraries are called Dynamic Link
Libraries
(DLLs). Code can be compiled into a .dll
file, which is then loaded in at
run-time by other executables on the system.
You might find yourself wanting to fuzz a Windows DLL. Like any other piece of software, a shared library would benefit from undergoing fuzzing. Currently, fuzzing a Windows DLL is possible, but slightly trickier. Read on!
How do I Build and Fuzz a DLL?
Follow these steps to build your DLL for fuzzing and build your fuzzing targets to invoke it.
Set up your Fuzzing Cargo.toml
Add your DLL's Cargo project to your fuzzing Cargo.toml
as an optional
dependency:
[dependencies]
# ...
your_dll = { path = "..", optional = true }
# ...
Add a feature to your Cargo.toml
that requires your DLL as a dependency. We
want the DLL to be built only when this feature is enabled:
[features]
# ...
build_your_dll = [
"dep:your_dll"
]
# ...
Finally, create a fuzzing target in your Cargo.toml
that requires this
feature. This will be a "dummy" fuzzing target whose sole purpose is to build
your DLL. It won't actually do any fuzzing; it's merely a way to have
cargo-fuzz build your DLL with AddressSanitizer (and other) instrumentation.
[[bin]]
name = "build_your_dll"
path = "fuzz_targets/build_your_dll.rs"
required-features = ["build_your_dll"]
test = false
doc = false
bench = false
Create the "Dummy" Fuzzing Target
Next, you need to create the source code for this "dummy" fuzzing target
(fuzz_targets/build_your_dll.rs
). At its simplest, all you need to do is
create a simple main
function:
pub fn main() { println!("DLL build complete!"); }
This "dummy" target will have its main function executed after the DLL build has completed, so if you'd like, you can add extra code here to perform any post-build installation or setup. (For example, perhaps you need to copy the built DLL to somewhere else on the system, in order for the fuzzing targets to find it.)
Create your Fuzzing Targets
After that, it's cargo-fuzz business as usual: create your fuzzing targets in
Cargo.toml
, and have them load and invoke your DLL:
[[bin]]
name = "fuzz_your_dll_1"
path = "fuzz_targets/fuzz_your_dll_1.rs"
test = false
doc = false
bench = false
Build the DLL and Run
To build the DLL, then run a fuzzing target, there are two separate commands you need to invoke:
# Build the DLL with your "dummy" target
cargo fuzz run --features=build_your_dll --no-include-main-msvc --strip-dead-code build_your_dll
(See the "Technical Details" for more information on why these options are needed.)
# Run your fuzzing target, now that your DLL is built
cargo fuzz run fuzz_your_dll_1
Technical Details
(Why do we have to fuzz DLLs this way? Click here to see some details.)
Code that is fuzzed through cargo-fuzz must be compiled with extra instrumentation inserted. The binary that is produced behaves normally, but executes additional code that cargo-fuzz (which uses LibFuzzer under the hood) can use to recieve feedback about how the target program behaved when given inputs from the fuzzer. In this way, a fuzzing "feedback loop" is established, and the fuzzer can slowly generate more "interesting" inputs that create new behavior in the target program.
In our case, the target program is a Windows DLL. Because it's a DLL
(shared library), it must be built and instrumented as a completely separate
binary (a .dll
file) from any fuzzing target executable (.exe
) we've
developed to test it. Your fuzzing target programs
(..../fuzz/fuzz_target/*.rs
) are calling functions from this DLL, but the
actual loading of those functions into the same process will occur at run-time.
So, there are two steps that need to be done when building (hence the two
separate cargo fuzz run ...
commands listed above):
- Build the DLL and install it.
- Build the fuzzing targets.
MSVC and LibFuzzer's main
Function
On Windows, Rust uses the MSVC compiler and
linker
to build. The cargo-fuzz fuzzing targets do not implement a main
function;
instead, they use LibFuzzer's built-in main
function. (This function is what
actually starts up the fuzzer. The fuzzer then invokes the fuzz_target!()
macro function defined in each fuzzing target.) The MSVC linker does not
seem to recognize the LibFuzzer main
function, and thus cannot build the
fuzzing targets without a little help.
To fix the problem, cargo-fuzz code adds
the /include:main
linker argument to the build arguments passed to cargo build
when it detects systems that are building with MSVC. This arguments
forces the inclusion of an external main
symbol in the executables produced by
MSVC. (See more on the /include
argument
here.)
This allows the fuzzing targets to build.
Adding /include:main
breaks DLL Linking
But hang on a second! DLLs by nature are shared libraries, and thus should not
have any references to a main
function. It's the job of the executable that
loads a DLL into worry about main
. So, if we attempt to build a DLL using
cargo fuzz build
, it'll add the /include:main
, and we'll get a linker error:
LINK : error LNK2001: unresolved external symbol main
C:/..../my_shared_library.dll : fatal error LNK1120: 1 unresolved externals
To avoid this, we use the --no-include-main-msvc
argument, which allows us to
control whether or not /include:main
is added to the MSVC linker arguments.
But removing /include:main
breaks Fuzzing Target Linking
However... we need /include:main
to build the fuzzing target executables. This
puts us at a bit of an impasse:
- If we add
/include:main
, the fuzzing targets will build, but the DLL will not. - If we remove
/include:main
, the DLL will build, but the fuzzing targets will not.
Solution: Two Separate Builds
To solve this, we need to invoke cargo fuzz ...
twice: once to build the DLL
(without /include:main
), and another time to build the fuzzing targets
(with /include:main
). In order to build the DLL using cargo-fuzz (which we
want to do, because it builds using all the relevant LLVM coverage and
AddressSanitizer compiler options), we implement a small "dummy" fuzzing target
that provides its own main
function.
This "dummy" target does not implement a fuzz_target!()
macro function (and
thus, no actual fuzzing occurs), but it acts as a vehicle for us to build the
Windows DLL for fuzzing. Plus, you can add any extra code to this "dummy" target
to help install your newly-built DLL in the correct location on your Windows
system.
Why Use --strip-dead-code
?
By default, cargo-fuzz invokes rustc with the -Clink-dead-code
argument.
This, as described
here,
controls whether or not the linker is instructed to keep dead code. "Dead code"
refers to functions/symbols that are provided by some dependency (such as a
DLL) but aren't ever referenced/used by the program that's importing code from
the dependency. This can be useful in some cases, but harmful in others.
In the case of the certain DLLs, it may be harmful. By building with
-Clink-dead-code
, references to unused functions/symbols within various
Windows DLLs your target DLL is dependent on would be included in the resulting
binary when you build it with cargo-fuzz.
For example: in windows-rs, the
Cryptography sub-crate (windows::Win32::Security::Cryptography
) includes
symbols from infocardapi.dll
). This DLL appears to no longer be supported, or
even installed on Windows. If -Clink-dead-code
were to cause these symbols to
be included in your DLL, loading will fail at run-time when, inevitably, those
symbol references can't be found, since infocardapi.dll
is nowhere to be
found on the system. (Your fuzzing target program will fail with
STATUS_DLL_NOT_FOUND
.)
This issue can be fixed by adding --strip-dead-code
to your cargo-fuzz
command, which removes the usage of -Clink-dead-code
when building.
Fuzzing with afl.rs
American fuzzy lop (AFL) is a popular, effective, and modern fuzz testing tool. afl.rs allows one to run AFL on code written in the Rust programming language.
Setup
Requirements
Tools
- C compiler (e.g. gcc or clang)
- make
Platform
afl.rs works on x86-64 Linux, x86-64 macOS, and ARM64 macOS.
cargo install cargo-afl
Alternatively, cargo-afl
can be installed from source.
Upgrading
cargo install --force cargo-afl
Tutorial
For this tutorial, we are going to fuzz the URL parser rust-url. Our goal here is to find some input generated by the fuzzer such that, when passed to Url::parse
, it causes some sort of panic or crash to happen.
Create a fuzz target
The first thing we’ll do is create a fuzz target in the form of a Rust binary crate. AFL will call the resulting binary, supplying generated bytes to standard input that we’ll pass to Url::parse
.
cargo new --bin url-fuzz-target
cd url-fuzz-target
We’ll need two dependencies in this crate:
url
: the crate we’re fuzzingafl
: not required, but includes a couple utility functions to assist in creating fuzz targets
So add these to the Cargo.toml
file:
[dependencies]
afl = "*"
url = "*"
Now we’ll need to write the source for the fuzz target in src/main.rs
:
#[macro_use]
extern crate afl;
extern crate url;
fn main() {
fuzz!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = url::Url::parse(&s);
}
});
}
fuzz!
is a utility macro provided by the afl
crate that reads bytes from standard input and
passes the bytes to the provided closure.
In the body of the closure, we call Url::parse
with the bytes that AFL generated. If all goes well, url::Url::parse
will return an Ok
containing a valid Url
, or an Err
indicating a Url
could not be constructed from the String
. If Url::parse
panics while parsing the String
, AFL will treat it as a crash and the AFL UI will indicate as such.
One important detail about the fuzz!
macro: if a panic occurs within the body of the closure, the panic will be caught and process::abort
will be subsequently called. Without the call to process::abort
, AFL would not consider the unwinding panic to be a crash.
Build the fuzz target
Normally, one uses cargo build
to compile a Cargo-based Rust project. To get AFL to work with Rust, a few extra compiler flags need to be passed to rustc during the build process. To make this easier, there is an AFL cargo subcommand (provided by the afl
crate) that automatically passes these rustc flags for us. To use it, you’ll do something like:
cargo afl <cargo command>
Since we want to build this crate, we’ll run:
cargo afl build
Provide starting inputs
AFL doesn't strictly require starting inputs, but providing some can make AFL’s job easier since it won’t need to ‘learn’ what a valid URL looks like. To do this, we'll create a directory called in
with a few files (filenames don’t matter) containing valid URLs:
mkdir in
echo "tcp://example.com/" > in/url
echo "ssh://192.168.1.1" > in/url2
echo "http://www.example.com:80/foo?hi=bar" > in/url3
Start fuzzing
To begin fuzzing, we’ll run:
cargo afl fuzz -i in -o out target/debug/url-fuzz-target
The fuzz
subcommand of cargo-afl
is the primary interface for fuzzing Rust code with AFL. For those already familiar with AFL, the fuzz
subcommand of cargo-afl
is identical to running afl-fuzz
.
The -i
flag specifies a directory full of input files AFL will use as seeds.
The -o
flag specifies a directory AFL will write all its state and results to.
The last argument target/debug/url-fuzz-target
specifies the fuzz target binary AFL will call, supplying random bytes to standard input.
As soon as you run this command, you should see AFL’s interface start up:
For more information about this UI and what each of the sections mean, see this resource hosted on the AFL website.
AFL will run indefinitely, so if you want to quit, press CTRL-C
.
Reproducing
Once you have a few crashes collected from running your fuzzer, you can reproduce them by passing them in manually to your test case. This is typically done via stdin
. E.g. for url-fuzz-target
the command would be:
cargo afl run url-fuzz-target < out/default/crashes/crash_file
where out
is the -o
parameter from your fuzz command and crash_file
is an arbitrary file in the crashes
directory.
Installing from source
First, clone afl.rs:
git clone https://github.com/rust-fuzz/afl.rs
cd afl.rs
Next, checkout afl.rs's submodule (AFL++). Note that --recursive
is not required.
git submodule update --init
Finally, install cargo-afl
:
cargo install --path cargo-afl
Troubleshooting
If cargo-afl
is panicking, consider installing with --debug
and running cargo-afl
with RUST_BACKTRACE=1
, e.g.:
cargo install --path cargo-afl --debug
...
RUST_BACKTRACE=1 cargo afl ...
Adding --debug
to the cargo install
command causes cargo-afl
to produce more elaborate backtraces.
Trophy Case
A collection of bugs found in Rust code through fuzz testing can be found in the trophy-case repository.