Fuzzing
Fuzzing is the process of providing random data to programs to identify unexpected behavior, such as crashes and panics.
Fuzz tests can also be written as property tests that instead of seeking to identify panics and crashes, assert on some property remaining true. Fuzzing as demonstrated here and elsewhere in these docs will use principles from both property testing and fuzzing, but will only use the term fuzzing to refer to both.
The following steps can be used in any Stellar contract workspace. If experimenting, try them in the increment example. The contract has an increment function that increases a counter value by one on every invocation.
See the Rust Fuzz Book for a general tutorial on using both cargo-fuzz and cargo-afl.
How to Write Fuzz Tests with cargo-fuzz
-
Install the nightly Rust toolchain. Nightly Rust is required to run cargo-fuzz.
rustup install nightly -
Install
cargo-fuzz.cargo install --locked cargo-fuzz -
Initialize a fuzz project by running the following command inside your contract directory.
cargo fuzz init -
Open the contract's
Cargo.tomlfile. Addlibas acrate-type.[lib]-crate-type = ["cdylib"]+crate-type = ["lib", "cdylib"] -
Open the generated
fuzz/Cargo.tomlfile. Add thesoroban-sdkdependency.[dependencies]libfuzzer-sys = "0.4"+soroban-sdk = { version = "*", features = ["testutils"] } -
Open the generated
fuzz/src/fuzz_target_1.rsfile. It will look like the below.#![no_main]use libfuzzer_sys::fuzz_target;fuzz_target!(|data: &[u8]| {// fuzzed code goes here}); -
Fill out the
fuzz_target!call with test setup and assertions. For example, for the increment example:#![no_main]use libfuzzer_sys::fuzz_target;use soroban_increment_with_fuzz_contract::{IncrementContract, IncrementContractClient};use soroban_sdk::{testutils::arbitrary::{arbitrary, Arbitrary},Env,};#[derive(Debug, Arbitrary)]pub struct Input {pub by: u64,}fuzz_target!(|input: Input| {let env = Env::default();let id = env.register(IncrementContract, ());let client = IncrementContractClient::new(&env, &id);let mut last: Option<u32> = None;for _ in input.by.. {match client.try_increment() {Ok(Ok(current)) => assert!(Some(current) > last),Err(Ok(_)) => {} // Expected errorOk(Err(_)) => panic!("success with wrong type returned"),Err(Err(_)) => panic!("unrecognised error"),}}}); -
Execute the fuzz target.
cargo +nightly fuzz run --sanitizer=thread fuzz_target_1infoIf you're developing on MacOS you need to add the
--sanitizer=threadflag in order to work around a known issue.
This test uses the same patterns used in unit tests and integration tests:
- Create an environment, the
Env. - Register the contract to be tested.
- Invoke functions using a client.
- Assert expectations.
For a full detailed example, see the fuzzing example.
How to Write Fuzz Tests with cargo-afl
cargo-afl drives AFL++ and, unlike cargo-fuzz, runs on stable Rust.
-
Install
cargo-afl. Installing it builds AFL++ from source, so a C compiler needs to be available.cargo install cargo-afl --locked -
Configure the machine for fuzzing.
cargo afl system-config -
Open the contract's
Cargo.tomlfile. Addlibas acrate-type. The fuzz target imports the contract as a Rust library.[lib]-crate-type = ["cdylib"]+crate-type = ["lib", "cdylib"] -
Create a fuzz target crate, for example with
cargo new --bin fuzzinside your contract's directory. Unlike acargo-fuzztarget, an AFL++ target depends on thearbitrarycrate directly, because thefuzz!macro expands to code that refers to it by an absolute path, which only resolves ifarbitraryis a direct dependency. Put the following in the new crate'sCargo.toml. It replaces the empty[dependencies]table thatcargo newgenerated.[dependencies]afl = "0.18"arbitrary = { version = "~1.3.0", features = ["derive"] }soroban-sdk = { version = "*", features = ["testutils"] }# The contract to fuzz. Use the package name from its Cargo.toml.soroban-increment-contract = { path = ".." }[[bin]]name = "fuzz_target_1"path = "src/fuzz_target_1.rs"# Prevent this from interfering with the contract's workspace, if it has one.[workspace]members = ["."] -
Write the fuzz target at
src/fuzz_target_1.rs, and delete the defaultsrc/main.rscargo newcreated. An AFL++ target is a regular binary crate with amainfunction that calls theafl::fuzz!macro. For example, for the increment example:use afl::fuzz;use arbitrary::Arbitrary;use soroban_increment_contract::{IncrementContract, IncrementContractClient};use soroban_sdk::Env;#[derive(Debug, Arbitrary)]pub struct Input {pub by: u8,}fn main() {fuzz!(|input: Input| {// Create the `Env` inside the closure, not outside: AFL++ reuses the// process for many inputs, and state created outside the closure// would leak from one input into the next.let env = Env::default();let id = env.register(IncrementContract, ());let client = IncrementContractClient::new(&env, &id);let mut last: Option<u32> = None;for _ in 0..input.by {match client.try_increment() {Ok(Ok(current)) => {assert!(Some(current) > last);last = Some(current);}Err(Ok(_)) => {} // Expected errorOk(Err(_)) => panic!("success with wrong type returned"),Err(Err(_)) => panic!("unrecognised error"),}}});} -
Build the target. The remaining steps also run from inside the
fuzzcrate.cd fuzzcargo afl build -
Fuzz the target, pointing at an input directory containing at least one seed input and an output directory to write results to.
mkdir in outecho -n '00000000' > in/seedcargo afl fuzz -i in -o out target/debug/fuzz_target_1
Crashing inputs are written to out/default/crashes/. The target reads an input on stdin when it isn't being driven by AFL++, so a crash can be replayed by feeding the file back in:
RUST_BACKTRACE=1 ./target/debug/fuzz_target_1 < out/default/crashes/id:000000*
How to Get Code Coverage of cargo-fuzz Tests
Getting code coverage data for fuzz tests requires some different tooling than when doing the same for regular Rust tests.
-
Run the
cargo-fuzztarget until it has produced a corpus.cargo +nightly fuzz run --sanitizer thread fuzz_target_1 -
Install the llvm-tools for the nightly compiler.
rustup component add --toolchain nightly llvm-tools-preview -
Run the fuzz coverage command that'll execute the corpus and write coverage data to the coverage directory in the
profdataformat.cargo +nightly fuzz coverage --sanitizer thread fuzz_target_1 -
Run the llvm-cov command to convert the profdata file to an lcov file.
$(find $(rustc --print sysroot) -name llvm-cov) export \-instr-profile=fuzz/coverage/fuzz_target_1/coverage.profdata \-object target/$(rustc -vV | sed -n 's|host: ||p')/coverage/$(rustc -vV | sed -n 's|host: ||p')/release/fuzz_target_1 \--ignore-filename-regex "rustc" \-format=lcov \> lcov.infoLoad the
lcov.infofile into your IDE using its coverage feature. In VSCode this can be done by installing the Coverage Gutters extension and executing theCoverage Gutters: Watchcommand.
To measure code coverage of regular Rust tests, see Code Coverage.
Guides in this category:
Unit Tests
Unit tests are small tests that test smart contracts.
Mocking
Mocking dependency contracts in tests.
Test Authorization
Write tests that test contract authorization.
Test Events
Write tests that test contract events.
Integration Tests
Integration testing uses dependency contracts instead of mocks.
Fork Testing
Integration testing using mainnet data.
Fuzzing
Fuzzing and property testing to find unexpected behavior.
Differential Tests
Differential testing detects unintended changes.
Differential Tests with Test Snapshots
Differential testing using automatic test snapshots.
Mutation Testing
Mutation testing finds code not tested.
Code Coverage
Code coverage tools find code not tested.
Testing with Ledger Snapshot
Use ledger snapshots to test contracts with ledger data