Skip to content
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
2 changes: 1 addition & 1 deletion devenv.lock
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"pre-commit-hooks",
"nixpkgs"
]
},
Expand Down
2 changes: 1 addition & 1 deletion devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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 @@ -22,15 +22,16 @@ capnp = { workspace = true }
derive_more = { workspace = true, features = ["display", "error", "from"] }
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 }
rustc-hash.workspace = true
semver = { 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
9 changes: 9 additions & 0 deletions hugr-model/src/v0/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub use resolve::ResolveError;
///
/// [`table::Package`]: crate::v0::table::Package
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Package {
/// The sequence of modules in the package.
pub modules: Vec<Module>,
Expand Down Expand Up @@ -70,6 +71,7 @@ impl Package {
///
/// [`table::Module`]: crate::v0::table::Module
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Module {
/// The root region of the module.
///
Expand Down Expand Up @@ -103,6 +105,7 @@ impl Module {
///
/// [`table::Node`]: crate::v0::table::Node
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Node {
/// The operation that the node performs.
pub operation: Operation,
Expand All @@ -129,6 +132,7 @@ pub struct Node {
///
/// [`table::Operation`]: crate::v0::table::Operation
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub enum Operation {
/// Invalid operation to be used as a placeholder.
#[default]
Expand Down Expand Up @@ -193,6 +197,7 @@ impl Operation {
///
/// [`table::Symbol`]: crate::v0::table::Symbol
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Symbol {
/// The visibility of the symbol.
pub visibility: Option<Visibility>,
Expand All @@ -212,6 +217,7 @@ pub struct Symbol {
///
/// [`table::Param`]: crate::v0::table::Param
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Param {
/// The name of the parameter.
pub name: VarName,
Expand All @@ -225,6 +231,7 @@ pub struct Param {
///
/// [`table::Region`]: crate::v0::table::Region
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct Region {
/// The kind of the region. See [`RegionKind`] for details.
pub kind: RegionKind,
Expand All @@ -249,6 +256,7 @@ pub struct Region {
///
/// [`table::Term`]: crate::v0::table::Term
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub enum Term {
/// Standin for any term.
#[default]
Expand Down Expand Up @@ -279,6 +287,7 @@ impl From<Literal> for Term {
///
/// [`table::SeqPart`]: crate::v0::table::SeqPart
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub enum SeqPart {
/// An individual item in the sequence.
Item(Term),
Expand Down
5 changes: 5 additions & 0 deletions hugr-model/src/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ impl<'py> pyo3::IntoPyObject<'py> for ScopeClosure {

/// The kind of a region.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub enum RegionKind {
/// Data flow region.
#[default]
Expand Down Expand Up @@ -447,6 +448,7 @@ impl<'py> pyo3::IntoPyObject<'py> for RegionKind {

/// The name of a variable.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct VarName(SmolStr);

impl VarName {
Expand Down Expand Up @@ -483,6 +485,7 @@ impl<'py> pyo3::IntoPyObject<'py> for &VarName {

/// The name of a symbol.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct SymbolName(SmolStr);

impl SymbolName {
Expand All @@ -508,6 +511,7 @@ impl<'py> pyo3::FromPyObject<'py> for SymbolName {

/// The name of a link.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub struct LinkName(SmolStr);

impl LinkName {
Expand Down Expand Up @@ -555,6 +559,7 @@ impl<'py> pyo3::IntoPyObject<'py> for &LinkName {
/// sequences of arbitrary length. To enable cheap cloning and sharing,
/// strings and byte sequences use reference counting.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature="arbitrary", derive(arbitrary::Arbitrary))]
pub enum Literal {
/// String literal.
Str(SmolStr),
Expand Down
Loading