Skip to content
Open
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
9 changes: 6 additions & 3 deletions rust/cargo_fuzztest/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ rust_clippy(
)

rust_test(
name = "sample_fuzz_test_bin",
name = "sample_fuzz_crate_bin",
srcs = ["test_crates/sample_fuzz_crate/src/lib.rs"],
edition = "2024",
tags = [
Expand All @@ -77,9 +77,12 @@ rust_test(

rust_test(
name = "runner_test",
srcs = ["tests/runner_test.rs"],
srcs = [
"tests/common/mod.rs",
"tests/runner_test.rs",
],
data = [
":sample_fuzz_test_bin",
":sample_fuzz_crate_bin",
],
edition = "2024",
deps = [
Expand Down
17 changes: 15 additions & 2 deletions rust/cargo_fuzztest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ fuzztest = { path = ".." }
tempfile = "3.27.0"

# Compiled as a [[test]] target so Cargo builds it with the test harness enabled
# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_test_bin` to `runner_test`.
# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_crate_bin` to `runner_test`.
[[test]]
name = "sample_fuzz_test_bin"
name = "sample_fuzz_crate_bin"
path = "test_crates/sample_fuzz_crate/src/lib.rs"
test = false

# Compiled as a [[test]] target so Cargo builds it with the test harness enabled
# (`rustc --test`) and exposes `CARGO_BIN_EXE_another_sample_fuzz_crate_bin` to `runner_test`.
[[test]]
name = "another_sample_fuzz_crate_bin"
path = "test_crates/another_sample_fuzz_crate/src/lib.rs"
test = false

[[test]]
name = "runner_test"
path = "tests/runner_test.rs"

[[test]]
name = "e2e_cli_test"
path = "tests/e2e_cli_test.rs"

208 changes: 199 additions & 9 deletions rust/cargo_fuzztest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

use anyhow::{Context, Result};
use clap::Parser;
pub use fuzztest_options::{ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions};
pub use fuzztest_options::{
ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCrashOptions,
};
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -54,7 +56,15 @@ impl CargoFuzzTestOptions {
fn check_centipede_binary_path_is_set(&self) -> Result<()> {
anyhow::ensure!(
self.centipede_binary_path.is_some(),
"fuzzing mode requires `--centipede-binary-path` to be specified"
"`--centipede-binary-path` needs to be specified"
);
Ok(())
}

fn check_corpus_db_is_set(&self) -> Result<()> {
anyhow::ensure!(
self.fuzztest_options.corpus_db.is_some(),
"`--corpus-db` needs to be specified"
);
Ok(())
}
Expand All @@ -72,14 +82,18 @@ impl CargoFuzzTestOptions {
self.check_centipede_binary_path_is_set()?;
mode
}
ExecutionMode::ReplayAllCrashes | ExecutionMode::ReplayCrash(_) => {
self.check_centipede_binary_path_is_set()?;
self.check_corpus_db_is_set()?;
mode
}
ExecutionMode::SmokeTest => {
if self.test_path.is_some() {
self.check_centipede_binary_path_is_set()?;
return Ok(ExecutionMode::Fuzz(FuzzOptions {
fuzz_for: FuzzFor::Indefinitely,
// TODO(the-shank): support parallel jobs
jobs: None,
}));
ExecutionMode::Fuzz(FuzzOptions {
fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely),
jobs: self.fuzztest_options.jobs,
})
} else {
mode
}
Expand Down Expand Up @@ -227,7 +241,7 @@ impl FuzztestRunner {
}

ExecutionMode::Fuzz(fuzz_options) => {
let FuzzOptions { fuzz_for, jobs: _ } = fuzz_options;
let FuzzOptions { fuzz_for, jobs } = fuzz_options;
match fuzz_for {
FuzzFor::Indefinitely => {
cmd.env("FUZZTEST_FUZZ_FOR", "inf");
Expand All @@ -236,7 +250,17 @@ impl FuzztestRunner {
cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string());
}
}
// TODO(the-shank): support parallel jobs
if let Some(jobs) = jobs {
cmd.env("FUZZTEST_JOBS", jobs.to_string());
}
}

ExecutionMode::ReplayCrash(replay_options) => {
cmd.env("FUZZTEST_REPLAY_ID", replay_options.replay_id);
}

ExecutionMode::ReplayAllCrashes => {
cmd.env("FUZZTEST_REPLAY_FINDINGS", "true");
}

ExecutionMode::SmokeTest => {
Expand All @@ -247,13 +271,26 @@ impl FuzztestRunner {
}
}

if let Some(corpus_db) = &self.options.fuzztest_options.corpus_db {
cmd.env("FUZZTEST_CORPUS_DB", corpus_db);
}

if let Some(workdir_root) = &self.options.fuzztest_options.workdir_root {
cmd.env("FUZZTEST_WORKDIR_ROOT", workdir_root);
}

// If `--centipede-binary-path` was passed to `cargo-fuzztest`, forward it to the
// child test executable via `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable so the
// fuzzer runtime can locate and run Centipede during continuous fuzzing.
if let Some(centipede_binary_path) = &self.options.centipede_binary_path {
cmd.env("FUZZTEST_CENTIPEDE_BINARY_PATH", centipede_binary_path);
}

if self.options.fuzztest_options.print_subprocess_log {
cmd.env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true");
cmd.arg("--nocapture");
}

Ok(cmd)
}

Expand Down Expand Up @@ -295,6 +332,7 @@ impl FuzztestRunner {
mod tests {
use super::*;
use googletest::prelude::*;
use std::collections::HashMap;

#[gtest]
fn test_parse_host_triple_valid() {
Expand Down Expand Up @@ -377,4 +415,156 @@ mod tests {
ExecutionMode::SmokeTest
);
}

#[gtest]
fn test_cli_option_parsing_replay_id_success() {
let parsed = CargoFuzzTestOptions::try_parse_from([
"cargo-fuzztest",
"--replay-id",
"crash_12345",
"--corpus-db",
"/tmp/corpus_db",
"--centipede-binary-path",
"/custom/centipede",
])
.expect("valid replay options should parse successfully");

assert_eq!(parsed.fuzztest_options.replay_id.as_deref(), Some("crash_12345"));
assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));

let mode = parsed.execution_mode().expect("valid execution mode");
assert_eq!(
mode,
ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: "crash_12345".to_string() })
);
}

#[gtest]
fn test_execution_mode_replay_id_missing_corpus_db_errors() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_id: Some("crash_12345".to_string()),
..Default::default()
},
centipede_binary_path: Some("/custom/centipede".to_string()),
..Default::default()
};
let err = options.execution_mode().expect_err("missing corpus-db should cause error");
assert!(err.to_string().contains("`--corpus-db` needs to be specified"));
}

#[gtest]
fn test_execution_mode_replay_id_missing_centipede_binary_path_errors() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_id: Some("crash_12345".to_string()),
corpus_db: Some("/tmp/corpus_db".to_string()),
..Default::default()
},
..Default::default()
};
let err =
options.execution_mode().expect_err("missing centipede-binary-path should cause error");
assert!(err.to_string().contains("`--centipede-binary-path` needs to be specified"));
}

#[gtest]
fn test_build_run_command_replay_crash() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_id: Some("crash_12345".to_string()),
corpus_db: Some("/tmp/corpus_db".to_string()),
..Default::default()
},
centipede_binary_path: Some("/custom/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");

let envs: HashMap<String, Option<String>> = cmd
.get_envs()
.map(|(k, v)| {
(k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
})
.collect();

expect_eq!(envs.get("FUZZTEST_REPLAY_ID").and_then(|v| v.as_deref()), Some("crash_12345"));
expect_eq!(
envs.get("FUZZTEST_CORPUS_DB").and_then(|v| v.as_deref()),
Some("/tmp/corpus_db")
);
expect_eq!(
envs.get("FUZZTEST_CENTIPEDE_BINARY_PATH").and_then(|v| v.as_deref()),
Some("/custom/centipede")
);
}

#[gtest]
fn test_cli_option_parsing_replay_findings_success() {
let parsed = CargoFuzzTestOptions::try_parse_from([
"cargo-fuzztest",
"--replay-findings",
"--corpus-db",
"/tmp/corpus_db",
"--centipede-binary-path",
"/custom/centipede",
])
.expect("valid replay-findings options should parse successfully");

assert!(parsed.fuzztest_options.replay_findings);
assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));

let mode = parsed.execution_mode().expect("valid execution mode");
assert_eq!(mode, ExecutionMode::ReplayAllCrashes);
}

#[gtest]
fn test_execution_mode_replay_findings_missing_centipede_binary_path_errors() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_findings: true,
corpus_db: Some("/tmp/corpus_db".to_string()),
..Default::default()
},
..Default::default()
};
let err =
options.execution_mode().expect_err("missing centipede-binary-path should cause error");
assert!(err.to_string().contains("`--centipede-binary-path` needs to be specified"));
}

#[gtest]
fn test_build_run_command_replay_all_crashes() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_findings: true,
corpus_db: Some("/tmp/corpus_db".to_string()),
..Default::default()
},
centipede_binary_path: Some("/custom/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");

let envs: HashMap<String, Option<String>> = cmd
.get_envs()
.map(|(k, v)| {
(k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
})
.collect();

expect_eq!(envs.get("FUZZTEST_REPLAY_FINDINGS").and_then(|v| v.as_deref()), Some("true"));
expect_eq!(
envs.get("FUZZTEST_CORPUS_DB").and_then(|v| v.as_deref()),
Some("/tmp/corpus_db")
);
expect_eq!(
envs.get("FUZZTEST_CENTIPEDE_BINARY_PATH").and_then(|v| v.as_deref()),
Some("/custom/centipede")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "another_sample_fuzz_crate"
version = "0.1.0"
edition = "2024"

[dependencies]
fuzztest = { path = "../../.." }
googletest = "0.14.3"

[workspace]

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use fuzztest::domains::arbitrary::Arbitrary;
use fuzztest::fuzztest;

#[fuzztest(data = Arbitrary::<i32>::default())]
fn sample_fuzztest_target(data: i32) {
let _ = data;
}

#[fuzztest(data = Arbitrary::<i32>::default())]
fn another_sample_fuzztest_target(data: i32) {
let _ = data;
}

#[fuzztest(data = Arbitrary::<i32>::default())]
fn jobs_fuzztest_target(_data: i32) {
println!("JOBS_TEST_PID: {}", std::process::id());
}

#[fuzztest(data = Arbitrary::<u8>::default())]
fn crashing_fuzztest_target(data: u8) {
if data == 10 {
panic!("Crashing bug found!");
}
}
6 changes: 6 additions & 0 deletions rust/cargo_fuzztest/test_crates/sample_fuzz_crate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ fn sample_fuzztest_target(data: i32) {
fn another_sample_fuzztest_target(data: i32) {
let _ = data;
}

#[fuzztest(_a = Arbitrary::<i32>::default())]
fn jobs_test(_a: i32) {
println!("JOBS_TEST_PID: {}", std::process::id());
std::thread::sleep(std::time::Duration::from_millis(250));
}
Loading
Loading