Quick Start
Setup
-
You can only use fuzzcheck on Linux or macOS (Windows support is possible but I need help with it)
-
Install
cargo-fuzzcheckcargo install cargo-fuzzcheck -
Make sure you are using Rust nightly
-
Add the following to
Cargo.toml:
[target.'cfg(fuzzing)'.dev-dependencies]
fuzzcheck = "0.12"
-
Add
serde = { version = "1.0", features = ["derive"] }to your dependencies as well- note: fuzzcheck has a serde dependency only when the
serde_json_serializerfeature is enabled. This feature is enabled by default, but you can write the following in yourCargo.tomlto disable it:
fuzzcheck = { version = "0.12", default-features = false } - note: fuzzcheck has a serde dependency only when the
-
Add
#![cfg_attr(fuzzing, feature(no_coverage))]at the top of the root module (e.g.src/lib.rsfor a library target)- fuzzcheck’s procedural macros use the
no_coverageattribute
- fuzzcheck’s procedural macros use the
-
In your library, integration test, or executable, create a test module, gated by
#[cfg(all(fuzzing, test))]and add a#[test]function inside it#[cfg(all(fuzzing, test))] mod tests { #[test] fn my_fuzz_test() { // ... } }We will write its body later on.
-
Create a test function of type
Fn(&T)orFn(&T) -> booland put the code you want to test inside it. You want this function to always succeed if the code is correct.// for example fn should_always_succeed(x: &SomeType) -> bool { // ... }
Mutator
The Mutator is responsible for generating values to feed to the test function.
The easiest way to get a mutator for a type is to use the #[derive(fuzzcheck::DefaultMutator)] attribute. Many std types also implement DefaultMutator.
Click here to reveal a code snippet showing a use of `DefaultMutator`
// example
use fuzzcheck::DefaultMutator;
#[derive(Clone, DefaultMutator)]
pub struct SomeType<A, B: SomeTrait> {
x: Option<A>,
y: bool,
z: Vec<Option<SomeOtherType<B>>>
}
#[derive(Clone, DefaultMutator)]
pub enum SomeOtherType<T> where T: SomeTrait {
A,
B { x: bool, y: Box<T> }
}
-
If the test case is a
String, you have two options:- Use the default mutator for
String, which is essentially a wrapper around aVec<u8>mutator and thus doesn't often generate useful strings - Use a grammar-based mutator (see tutorial 2 or the
fuzzcheck::mutators::grammarmodule documentation)
- Use the default mutator for
-
If the argument is a recursive type, you will need to use the
make_mutator!macro (see thefuzzcheck::mutators::recursivemodule documentation) -
otherwise, you may need to write the mutator yourself
- or ask me to do it on GitHub, if it's a type from
std
- or ask me to do it on GitHub, if it's a type from
Serializer
You need a way to save the generated test cases to the file system. This is most easily done
with the fuzzcheck::SerdeSerializer type if the test case implements Serialize and Deserialize:
use serde::{Serialize, Deserialize};
#[derive(/*..*/, Serialize, Deserialize)]
struct Foo { /* .. */}
But there are other choices:
ByteSerializerif the test case isVec<u8>and we simply want to copy the bytes from/to the filesStringSerializerif the test case is something that implementsFromStrandToString- your own serializer
Sensor and Pool
TODO (but you probably don't need to worry about it to get started)
In the meantime, you can look at the CodeCoverageSensorAndPoolBuilder documentation, and the fuzzcheck::sensors_and_pools module documentation.
Building the fuzz test
We go back to the #[test] fuzz function that we defined earlier and write its body.
If you used:
- the
DefaultMutatorderive attribute, or any other type that has a default mutator serdeto serialize the test cases
Then you can write:
#[cfg(all(fuzzing, test))]
mod tests {
#[test]
fn my_fuzz_test() {
let result = fuzzcheck::fuzz_test(should_always_succeed) // the name of the function to test
.default_options()
.launch();
assert!(!result.found_test_failure);
}
}
Otherwise, you can specify each component separately. Check the fuzzcheck::builder module documentation to learn about the different options available.
#[cfg(all(fuzzing, test))]
mod tests {
#[test]
fn my_fuzz_test() {
let mutator = /* ... */;
let serializer = /* ... */;
let (sensor, pool) = /* .. */;
let _ = fuzzcheck::fuzz_test(should_always_succeed) // the name of the function to test
.mutator(my_mutator) // or .default_mutator() for the default one
.serializer(my_serializer) // or .serde_serializer() for the default one
.sensor_and_pool(sensor, pool) // or .default_sensor_and_pool() for the default ones
.arguments_from_cargo_fuzzcheck() // take the other arguments from the `cargo fuzzcheck` invocation
.launch();
}
}
Launching the fuzz test
On the command line, with the current directory at the root of your crate, run cargo fuzzcheck <args..> to
launch the fuzz test. The mandatory arguments are:
-
the target to compile, i.e. where is the
#[test]function located?- the package’s library → add
--libor nothing - an integration test → add
--test <NAME> - an executable → add
--bin <NAME>
- the package’s library → add
-
the exact path to the test function
# example cargo fuzzcheck tests::my_fuzz_test
That's it for the essential arguments. You can run cargo fuzzcheck --help for a list of all possible arguments.
Once you run the command, the crate will be compiled and the fuzz test will start.
Terminal output
When the fuzz test is running, a line is printed after every notable event. It looks like this:
<time> <iter nbr> <pool_name>(<pool_stats>)... failures(..) iter/s <N>
where:
timeis the time elapsed since the startiter nbris the number of iterations performed so farpool_name(pool_stats)are statistics about the poolfailures(..)is the number of test failures founditer/sis the number of iterations performed every second
File System Output
The fuzzer creates files under the fuzz/<fuzz_test> folder. Each pool used by the fuzzer maintains
a copy of its content under fuzz/<fuzz_test>/corpus/<pool_name>. In particular, failing test cases
can be found at fuzz/<fuzz_test>/corpus/test_failures.
There is also a folder called stats, which saves information about each fuzzing run and can be read
by other tools for analysis.