About
Many popular fuzzers — including libfuzzer, AFL, and more — are
coverage-guided and mutation-based.
Coverage-guided means that the fuzzer observes which code is dynamically executed while running an input through the system under test. When creating new inputs, it will try to make inputs that execute new code paths, maximizing the amount of code that's been explored. If a new input triggers new code paths to be executed, then it is added to the corpus. If a new input only exercises code paths that have already been discovered, then it is thrown away.
Mutation-based means that, when creating a new input, the fuzzer modifies an existing input from its corpus. The idea is that, if the existing input triggered interesting behavior in the system under test, then a modification of that input probably will as well, but might additionally trigger some new behavior as well. Consider the scenario where we are fuzzing programming language's parser: if some input made it deep inside the parser, rather than bouncing off early due to an invalid token, then a new input derived from this one is also likely to go deep into the parser. At least it is more likely to do so than a new random string.
But what happens when we aren't fuzzing a language parser, or something else
that the fuzzer's built-in mutation strategies are pretty good at supporting?
When we have our own custom, structured input type? In this case, some fuzzers
will expose a hook for customizing the routine for mutating an existing input
from its corpus to create a new candidate input, for example the
libfuzzer::fuzz_mutator! hook. And mutatis exists to
simplify writing these custom mutation hooks.
Usage
There are two primary components to this library:
-
The
mutatis::Mutatortrait. A trait that is implemented by types which can mutate other types. Themutatis::Mutator::mutatetrait method takes a value and mutates it. You can think of amutatis::Mutatorimplementation like a streaming iterator that takes as input and modifies items, rather than generating them from scratch and returning them. -
The
mutatis::mutatorsmodule. This module, idiomatically imported withuse mutatis::mutators as m, provides a set of types and combinators for building custom mutators.
Here's an example of using mutatis to define a custom mutator for a simple
data structure:
#
Automatically Deriving Mutators with #[derive(Mutator)]
If you enable this crate's derive cargo feature, then you can automatically
derive mutators for your type definitions.
First, enable the derive feature in Cargo.toml:
[]
= { .., features = ["derive"] }
Then simply slap #[derive(Mutator)] onto your type definitions:
#
The generated mutator also has a constructor that takes sub-mutators for each field of the input type:
#
Container Attributes
The #[derive(Mutator)] macro supports the following attributes on structs
and enums:
-
#[mutatis(mutator_name = MyCoolName)]: Generate a mutator type namedMyCoolNameinstead of appendingMutatorto the input type's name. -
#[mutatis(mutator_doc = "my documentation")]: Generate a custom doc comment for the generated mutator type. This may be repeated multiple times. The resulting doc comment is a concatenation of all occurrences. -
#[mutatis(default_mutator = false)]: Do not implement theDefaultMutatortrait for the generated mutator type.
Field Attributes
The #[derive(Mutator)] macro suports the following attributes on fields within
structs and enum variants:
-
#[mutatis(ignore)]: Do not mutate this field. -
#[mutatis(default_mutator)]: Always use this field's type'sDefaultMutatorimplementation to mutate this field. Do not generate a generic type parameter or argument to the generated mutator's constructor for mutating this field.
Integrating Your Mutators with a Fuzzer
These are general steps for integrating your custom, mutatis-based mutators
into your fuzzing or property-based testing framework of choice:
-
Identify the framework's mechanism for customizing mutations, for example the
libfuzzer_sys::fuzz_mutator!macro. -
Implement that mechanism by:
-
Converting the framework's raw bytes for the test case into your structured test case type, if necessary.
Most fuzzers, including
libfuzzer, don't know anything about your structured types, they just manipulate byte buffers. So you'll need to convert the raw bytes into your structured test case type. If you don't otherwise have a natural way of doing that, like if you're fuzzing a parser and could just run the parser on the raw data, then a quick-and-easy trick to to useserdeandbincodeto deserialize the raw bytes into your structured type. -
Run your
mutatis-based custom mutator on the structured test case. -
Convert the structured test case back into raw bytes for the framework, if necessary.
This is the inverse of step (i). If you used
serdeandbincodein step (i) you would also want to use them here in step (iii).
-
Example with libfuzzer-sys
While mutatis is agnostic of which fuzzing engine or property-testing
framework you use, here's an example of using mutatis to define a custom
mutator for libfuzzer-sys. Integrating mutatis with other
fuzzing engines' APIs should look pretty similar, mutatis mutandis.
This example defines two different color representations, RGB and HSL, as well as conversions between them. The fuzz target asserts that roundtripping an RGB color to HSL and back to RGB correctly results in the original RGB color. The fuzz mutator converts the fuzzer's bytes into an RGB color, mutates the RGB color, and then updates the fuzzer's test case based on the mutated RGB color.
#
Shrinking Test Cases
You can configure a MutationContext to only perform mutations that "shrink"
their given values. When paired with a property or predicate function, doing so
lets you easily build test-case reducers that find the smallest input that
triggers a bug.
#
# foo.unwrap
When implementing Mutator by hand, rather than relying on the
mutatis::mutator module's combinators or the derive(Mutator) macro, be sure
to check whether context.shrink() returns true and adjust your mutation
strategy appropriately. Compared to the original input, a shrunken value should
be simpler and less complex, have fewer members inside its inner Vecs and
other container types, serialize to fewer bytes, etc...
#
# foo.unwrap
Writing Smoke Tests with mutatis::check
When you enable the check feature in Cargo.toml, the mutatis::check module
provides a tiny property-based testing framework that is suitable for writing
smoke tests. It is not intended to replace a full-fledged fuzzing engine that
you'd use for in-depth, 24/7 fuzzing.
Cargo Features
Note: none of this crate's features are enabled by default. You most likely
want to enable std.
-
alloc: ImplementMutators for types in Rust'salloccrate and internally use features that thealloccrate provides. -
std: ImplementMutators for types in Rust'sstdcrate and internally use features that thestdcrate provides. -
check: Enable themutatis::checkmodule for writing property-based smoke tests withmutatis. -
derive: Enable the#[derive(Mutator)]macro for automatically derivingMutatorimplementations for your types
Minimum Supported Rust Version
The minimum supported Rust version (MSRV) is currently 1.80.0.
The MSRV will never be increased in a patch release, but may be increased in a minor release. We will aim to avoid doing so without good reason.
License
Licensed under dual MIT or Apache-2.0 at your choice.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.