Skip to content

test: fuzz testing with cargo-fuzz #2316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 13 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ lto = "thin"
[workspace]
resolver = "2"
members = [
"hugr",
"hugr-core",
"hugr-passes",
"hugr-cli",
"hugr-model",
"hugr-llvm",
"hugr-py",
"hugr-persistent",
"hugr",
"hugr-core",
"hugr-passes",
"hugr-cli",
"hugr-model",
"hugr-llvm",
"hugr-py",
"hugr-persistent",
"fuzz",
]
default-members = ["hugr", "hugr-core", "hugr-passes", "hugr-cli", "hugr-model"]

Expand All @@ -25,10 +26,10 @@ license = "Apache-2.0"

[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
# Set by our CI
'cfg(ci_run)',
# Set by codecov
'cfg(coverage,coverage_nightly)',
# Set by our CI
'cfg(ci_run)',
# Set by codecov
'cfg(coverage,coverage_nightly)',
] }

missing_docs = "warn"
Expand Down
49 changes: 23 additions & 26 deletions devenv.lock
Original file line number Diff line number Diff line change
Expand Up @@ -51,31 +51,10 @@
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747372754,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"pre-commit-hooks",
"nixpkgs"
]
},
Expand Down Expand Up @@ -107,15 +86,33 @@
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1747372754,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"fenix": "fenix",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
"pre-commit-hooks": "pre-commit-hooks"
}
},
"rust-analyzer-src": {
Expand Down
2 changes: 1 addition & 1 deletion devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ in
# https://devenv.sh/languages/
# https://devenv.sh/reference/options/#languagesrustversion
languages.rust = {
channel = "stable";
channel = "nightly";
enable = true;
components = [ "rustc" "cargo" "clippy" "rustfmt" "rust-analyzer" ];
};
Expand Down
29 changes: 29 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "fuzz"
version = "0.0.0"
publish = false
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
hugr-model = { path = "../hugr-model/", features = ["arbitrary"] }

# [dependencies.hugr]
# path = ".."

[[bin]]
name = "fuzz_random"
path = "fuzz_targets/fuzz_random.rs"
test = false
doc = false
bench = false

[[bin]]
name = "fuzz_structure"
path = "fuzz_targets/fuzz_structure.rs"
test = false
doc = false
bench = false
64 changes: 64 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Fuzz testing

This project uses `cargo-fuzz` for doing fuzz testing for hugr.

## Requisites

1. Install `cargo-fuzz` with: `cargo install cargo-fuzz`
2. Build with `cargo fuzz build`

> [!NOTE]
> The `libFuzzer` used by `cargo-fuzz` needs **nightly**.

## Fuzz targets

You can list the fuzzing targets with:
`cargo fuzz list`

### Model: Random

The [fuzz_random](./fuzz_targets/fuzz_random.rs) target uses the coverage-guided
`libFuzzer` fuzzing engine to generate random bytes that we then try to
convert to a package with `hugr_model::v0::ast::Package::from_str()`.

To run this target:
`cargo fuzz run fuzz_random`

It is recommended to provide the `libFuzzer` with a corpus to speed up the
generation of test inputs. For this we can use the fixtures in
`hugr/hugr-model/tests/fixtures`:
`cargo fuzz run fuzz_random ../hugr-model/tests/fixtures`

If you want `libFuzzer` to mutate the examples with ascii characters only:
`cargo fuzz run fuzz_random -- -only_ascii=1`

### Model: Structure

The [fuzz_structure](./fuzz_targets/fuzz_structure.rs) target uses `libFuzzer` to do
[structure-aware](https://rust-fuzz.github.io/book/cargo-fuzz/structure-aware-fuzzing.html)
modifications of the `hugr_model::v0::ast::Package` and its members.

To run this target:
`cargo fuzz run fuzz_structure`

> [!NOTE]
> This target needs some slight modifications to the `hugr-model` source
> code so the structs and enums can derive the `Arbitrary` implementations
> needed by `libFuzzer`.
> The `arbitrary` features for `ordered-float` and `smol_str` are also needed.

## Results

The fuzzing process will be terminated once a crash is detected, and the offending input
will be saved to the `artifacts/<target>` directory. You can reproduce the crash by doing:
`cargo fuzz run fuzz_structure artifacts/<target>/crash-XXXXXX`

If you want to keep the fuzzing process, even after a crash has been detected,
you can provide the options `-fork=1` and `-ignore_crashes=1`.

## Providing options to `libFuzzer`

You can provide lots of options to `libFuzzer` by doing `cargo fuzz run <target> -- -flag1=val1 -flag2=val2`.

To see all the available options:
`cargo fuzz run <target> -- -help=1`
11 changes: 11 additions & 0 deletions fuzz/fuzz_targets/fuzz_random.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use hugr_model::v0 as model;
use std::str::FromStr;

fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _package_ast = model::ast::Package::from_str(&s);
}
});
14 changes: 14 additions & 0 deletions fuzz/fuzz_targets/fuzz_structure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#![no_main]

use hugr_model::v0 as model;
use libfuzzer_sys::fuzz_target;
use model::bumpalo::Bump;
use hugr_model::v0::ast::Package;

fuzz_target!(|package: Package| {
let bump = Bump::new();
let package = package.resolve(&bump).unwrap();
let bytes = model::binary::write_to_vec(&package);
let deserialized_package = model::binary::read_from_slice(&bytes, &bump).unwrap();
assert_eq!(package, deserialized_package);
});
5 changes: 3 additions & 2 deletions hugr-model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ derive_more = { workspace = true, features = ["display", "error", "from"] }
fxhash.workspace = true
indexmap.workspace = true
itertools.workspace = true
ordered-float = { workspace = true }
ordered-float = { workspace = true, features = ["arbitrary"] }
pest = { workspace = true }
pest_derive = { workspace = true }
pretty = { workspace = true }
smol_str = { workspace = true, features = ["serde"] }
smol_str = { workspace = true, features = ["serde", "arbitrary"] }
thiserror.workspace = true
pyo3 = { workspace = true, optional = true, features = ["extension-module"] }
arbitrary = { version = "1", optional = true, features = ["derive"] }

[features]
pyo3 = ["dep:pyo3"]
Expand Down
Loading
Loading