dev-fuzz 0.9.1

Fuzz testing workflow for Rust. Wraps cargo-fuzz: budgeted runs, crash / timeout / OOM findings, deterministic reproducer paths, sanitizer choice, machine-readable verdicts. Part of the dev-* verification collection.
Documentation

What it does

dev-fuzz wraps cargo-fuzz (libFuzzer-based) and emits findings as a dev-report::Report. Each finding carries the reproducer path that triggered it as Evidence::FileRef, so the input can be replayed and debugged.

What is fuzzing?

Fuzzing feeds random or guided-random inputs to your code looking for crashes, panics, or unexpected behavior. It's the standard tool for:

  • Parsers
  • Deserializers
  • Network protocol handlers
  • Anything that takes untrusted bytes

A typical fuzz session runs for minutes to hours and feeds billions of inputs through the target.

Quick start

[dependencies]
dev-fuzz = "0.9"

One-time tool install:

cargo install cargo-fuzz
rustup toolchain install nightly      # libFuzzer requires nightly

Drive it from code:

use dev_fuzz::{FuzzBudget, FuzzRun};
use std::time::Duration;

let run = FuzzRun::new("parse_input", "0.1.0")
    .budget(FuzzBudget::time(Duration::from_secs(60)));
let result = run.execute()?;
let report = result.into_report();
println!("{}", report.to_json()?);
# Ok::<(), Box<dyn std::error::Error>>(())

Builder surface

Method What it does
budget(FuzzBudget) Time- or executions-based budget. Default: 60s wall-clock.
in_dir(path) Run cargo fuzz from a different directory.
sanitizer(Sanitizer) Pick Address / Leak / Memory / Thread / None. Default Address.
timeout_per_iter(Duration) libFuzzer's -timeout=<secs>.
rss_limit_mb(u32) libFuzzer's -rss_limit_mb=<N>.
allow(name) / allow_all(iter) Suppress findings whose reproducer basename matches.

Budget types

FuzzBudget Maps to libFuzzer flag
time(Duration) -max_total_time=<secs>
executions(u64) -runs=<N>

Severity policy

FuzzFindingKind dev-report::Severity CheckResult verdict
Crash Critical Fail
OutOfMemory Error Fail
Timeout Warning Fail

Each finding emits a CheckResult named fuzz::<target>::<kind-label> (e.g. fuzz::parse_input::crash), tagged fuzz plus the kind label (crash / timeout / oom). The reproducer path rides along as Evidence::FileRef with the label "reproducer".

Allow-listing known false positives

use dev_fuzz::{FuzzBudget, FuzzRun};
use std::time::Duration;

let run = FuzzRun::new("parse_input", "0.1.0")
    .budget(FuzzBudget::time(Duration::from_secs(60)))
    .allow("crash-known-false-positive")
    .allow_all(["crash-cafebabe", "timeout-known-slow"]);

let _result = run.execute()?;
# Ok::<(), Box<dyn std::error::Error>>(())

Matches are on the basename of the reproducer path (the final component, typically crash-<hex> or timeout-<hex>).

Producer integration

FuzzProducer plugs the fuzz target into a multi-producer pipeline driven by dev-tools:

use dev_fuzz::{FuzzBudget, FuzzProducer, FuzzRun};
use dev_report::Producer;
use std::time::Duration;

let producer = FuzzProducer::new(
    FuzzRun::new("parse_input", "0.1.0")
        .budget(FuzzBudget::time(Duration::from_secs(60))),
);

let report = producer.produce();
println!("{}", report.to_json().unwrap());

Subprocess failures (missing tool, missing nightly, target not found, libFuzzer harness error) map to a single failing CheckResult named fuzz::<target> with Severity::Critical — the pipeline keeps running.

Wire format

FuzzResult, FuzzFinding, FuzzFindingKind, FuzzBudget, and Sanitizer are all serde-derived. JSON uses snake_case field names and lowercase enum variants:

{
  "target": "parse_input",
  "version": "0.1.0",
  "executions": 1234567,
  "findings": [
    {
      "kind": "crash",
      "reproducer_path": "fuzz/artifacts/parse_input/crash-deadbeef",
      "summary": "thread '<unnamed>' panicked at 'assertion failed'"
    }
  ]
}

Examples

File What it shows
examples/basic.rs Default 10s budget; graceful tool-missing handling.
examples/executions_budget.rs FuzzBudget::executions(N) instead of time.
examples/with_limits.rs Sanitizer choice, per-iter timeout, RSS limit, allow-list.
examples/producer.rs FuzzProducer (gated by DEV_FUZZ_EXAMPLE_RUN).

Requirements

Both prerequisites must be installed:

cargo install cargo-fuzz
rustup toolchain install nightly      # libFuzzer needs nightly

The crate detects absence of either and surfaces a typed FuzzError variant rather than panicking.

Runtime dependency footprint: dev-report, serde, serde_json.

Migration from 0.1.0

0.1.0 was a name-claim publish with a stub execute() returning an empty result. Existing builder calls (new, budget, execute, into_report) keep their signatures and behavior. New builder methods (in_dir, sanitizer, timeout_per_iter, rss_limit_mb, allow, allow_all) are additive.

The dev-* collection

dev-fuzz ships independently and is also re-exported by the dev-tools umbrella crate as the fuzz feature. Sister crates cover the other verification dimensions:

Status

v0.9.x is the pre-1.0 stabilization line. The crate is feature-complete for libFuzzer-based fuzzing (subprocess invocation, stderr parsing, severity policy, allow-list, sanitizer choice, producer integration). 1.0 will pin the public API and the wire format.

Minimum supported Rust version

1.85 for this crate. The user's fuzz targets require nightly Rust because libFuzzer instrumentation is nightly-only — that's a property of cargo-fuzz, not dev-fuzz.

License

Apache-2.0. See LICENSE.