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"

135 changes: 126 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::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,13 @@ 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::SmokeTest => {
Expand All @@ -247,13 +267,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 @@ -377,4 +410,88 @@ 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: Vec<(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();

assert!(envs.contains(&("FUZZTEST_REPLAY_ID".to_string(), Some("crash_12345".to_string()))));
assert!(
envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string())))
);
assert!(envs.contains(&(
"FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
Some("/custom/centipede".to_string())
)));
}
}
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));
}
45 changes: 45 additions & 0 deletions rust/cargo_fuzztest/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use cargo_fuzztest::FuzztestRunner;
use std::env;
use std::path::PathBuf;

// Helper to locate compiled sample fuzz test executables across different build environments
// (Cargo tests vs Blaze/Bazel tests).
pub fn get_sample_test_bin_path(crate_name: &str) -> PathBuf {
let test_bin_name = format!("{crate_name}_bin");

// 1. Cargo test: Check if CARGO_BIN_EXE_<name> is set by Cargo when running integration tests.
if let Ok(cargo_bin) = env::var(format!("CARGO_BIN_EXE_{test_bin_name}")) {
return PathBuf::from(cargo_bin);
}

// 2. Bazel/Blaze test: Fall back to TEST_SRCDIR and TEST_WORKSPACE.
if let Ok(src_dir) = env::var("TEST_SRCDIR") {
let test_workspace = env::var("TEST_WORKSPACE").unwrap_or_else(|_| {
"_main".to_string()
});

let relative_binary_path =
format!("rust/cargo_fuzztest/{test_bin_name}");

let blaze_path = PathBuf::from(src_dir).join(test_workspace).join(relative_binary_path);
assert!(blaze_path.exists());
return blaze_path;
}

// 3. Cargo test fallback: Query compiled test executable path via Cargo JSON compiler messages.
if env::var("CARGO_MANIFEST_DIR").is_ok() {
let cargo_bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let output = std::process::Command::new(cargo_bin)
.args(["test", "--no-run", "--message-format=json", "--test", &test_bin_name])
.output()
.expect("cargo test compilation command should execute successfully");
assert!(output.status.success());

let json_stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(exe) = FuzztestRunner::parse_compiler_messages(&json_stdout) {
return exe;
}
}

panic!("Could not locate {test_bin_name} via CARGO_BIN_EXE, TEST_SRCDIR, or Cargo compiler messages");
}
Loading
Loading