#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::time::Duration;
use dev_report::{CheckResult, Evidence, Report, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuzzFindingKind {
Crash,
Timeout,
OutOfMemory,
}
#[derive(Debug, Clone, Copy)]
pub enum FuzzBudget {
Time(Duration),
Executions(u64),
}
impl FuzzBudget {
pub fn time(d: Duration) -> Self {
Self::Time(d)
}
pub fn executions(n: u64) -> Self {
Self::Executions(n)
}
}
#[derive(Debug, Clone)]
pub struct FuzzRun {
target: String,
version: String,
budget: FuzzBudget,
}
impl FuzzRun {
pub fn new(target: impl Into<String>, version: impl Into<String>) -> Self {
Self {
target: target.into(),
version: version.into(),
budget: FuzzBudget::Time(Duration::from_secs(60)),
}
}
pub fn budget(mut self, budget: FuzzBudget) -> Self {
self.budget = budget;
self
}
pub fn fuzz_budget(&self) -> FuzzBudget {
self.budget
}
pub fn execute(&self) -> Result<FuzzResult, FuzzError> {
Ok(FuzzResult {
target: self.target.clone(),
version: self.version.clone(),
executions: 0,
findings: Vec::new(),
})
}
}
#[derive(Debug, Clone)]
pub struct FuzzFinding {
pub kind: FuzzFindingKind,
pub reproducer_path: String,
pub summary: String,
}
#[derive(Debug, Clone)]
pub struct FuzzResult {
pub target: String,
pub version: String,
pub executions: u64,
pub findings: Vec<FuzzFinding>,
}
impl FuzzResult {
pub fn into_report(self) -> Report {
let mut report = Report::new(&self.target, &self.version).with_producer("dev-fuzz");
if self.findings.is_empty() {
report.push(
CheckResult::pass(format!("fuzz::{}", self.target))
.with_detail(format!("{} executions, 0 findings", self.executions))
.with_evidence(Evidence::numeric_int("executions", self.executions as i64)),
);
} else {
for f in &self.findings {
let sev = match f.kind {
FuzzFindingKind::Crash => Severity::Critical,
FuzzFindingKind::Timeout => Severity::Warning,
FuzzFindingKind::OutOfMemory => Severity::Error,
};
let kind_str = match f.kind {
FuzzFindingKind::Crash => "crash",
FuzzFindingKind::Timeout => "timeout",
FuzzFindingKind::OutOfMemory => "oom",
};
report.push(
CheckResult::fail(format!("fuzz::{}::{kind_str}", self.target), sev)
.with_detail(f.summary.clone())
.with_evidence(Evidence::file_ref("reproducer", &f.reproducer_path)),
);
}
}
report.finish();
report
}
}
#[derive(Debug)]
pub enum FuzzError {
ToolNotInstalled,
NightlyRequired,
SubprocessFailed(String),
TargetNotFound(String),
}
impl std::fmt::Display for FuzzError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ToolNotInstalled => write!(f, "cargo-fuzz is not installed"),
Self::NightlyRequired => write!(f, "nightly Rust is required"),
Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
Self::TargetNotFound(s) => write!(f, "fuzz target not found: {s}"),
}
}
}
impl std::error::Error for FuzzError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_builds() {
let r = FuzzRun::new("parse", "0.1.0");
match r.fuzz_budget() {
FuzzBudget::Time(_) => {}
_ => panic!("default should be time-based"),
}
}
#[test]
fn executions_budget() {
let r = FuzzRun::new("parse", "0.1.0").budget(FuzzBudget::executions(1_000_000));
match r.fuzz_budget() {
FuzzBudget::Executions(n) => assert_eq!(n, 1_000_000),
_ => panic!("expected executions budget"),
}
}
#[test]
fn empty_findings_passes() {
let r = FuzzResult {
target: "parse".into(),
version: "0.1.0".into(),
executions: 1_000_000,
findings: Vec::new(),
};
let report = r.into_report();
assert!(report.passed());
}
#[test]
fn crash_finding_is_critical() {
let r = FuzzResult {
target: "parse".into(),
version: "0.1.0".into(),
executions: 500,
findings: vec![FuzzFinding {
kind: FuzzFindingKind::Crash,
reproducer_path: "fuzz/artifacts/crash-deadbeef".into(),
summary: "panic in parse_input".into(),
}],
};
let report = r.into_report();
assert!(report.failed());
assert_eq!(report.checks[0].severity, Some(Severity::Critical));
}
}