Skip to content

Commit 1aaf408

Browse files
the-shankcopybara-github
authored andcommitted
cargo-fuzztest: support replaying a crash by id
- Validate that --centipede-binary-path and --corpus-db are provided when replaying a crash. - Pass FUZZTEST_REPLAY_ID and FUZZTEST_CORPUS_DB environment variables to the target binary. - Add unit and integration tests for crash replay configuration. PiperOrigin-RevId: 956094189
1 parent 810d993 commit 1aaf408

11 files changed

Lines changed: 621 additions & 66 deletions

File tree

‎rust/cargo_fuzztest/BUILD‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ rust_clippy(
6262
)
6363

6464
rust_test(
65-
name = "sample_fuzz_test_bin",
65+
name = "sample_fuzz_crate_bin",
6666
srcs = ["test_crates/sample_fuzz_crate/src/lib.rs"],
6767
edition = "2024",
6868
tags = [
@@ -77,9 +77,12 @@ rust_test(
7777

7878
rust_test(
7979
name = "runner_test",
80-
srcs = ["tests/runner_test.rs"],
80+
srcs = [
81+
"tests/common/mod.rs",
82+
"tests/runner_test.rs",
83+
],
8184
data = [
82-
":sample_fuzz_test_bin",
85+
":sample_fuzz_crate_bin",
8386
],
8487
edition = "2024",
8588
deps = [

‎rust/cargo_fuzztest/Cargo.toml‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,24 @@ fuzztest = { path = ".." }
3030
tempfile = "3.27.0"
3131

3232
# Compiled as a [[test]] target so Cargo builds it with the test harness enabled
33-
# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_test_bin` to `runner_test`.
33+
# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_crate_bin` to `runner_test`.
3434
[[test]]
35-
name = "sample_fuzz_test_bin"
35+
name = "sample_fuzz_crate_bin"
3636
path = "test_crates/sample_fuzz_crate/src/lib.rs"
37+
test = false
38+
39+
# Compiled as a [[test]] target so Cargo builds it with the test harness enabled
40+
# (`rustc --test`) and exposes `CARGO_BIN_EXE_another_sample_fuzz_crate_bin` to `runner_test`.
41+
[[test]]
42+
name = "another_sample_fuzz_crate_bin"
43+
path = "test_crates/another_sample_fuzz_crate/src/lib.rs"
44+
test = false
3745

3846
[[test]]
3947
name = "runner_test"
4048
path = "tests/runner_test.rs"
49+
50+
[[test]]
51+
name = "e2e_cli_test"
52+
path = "tests/e2e_cli_test.rs"
53+

‎rust/cargo_fuzztest/src/lib.rs‎

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
use anyhow::{Context, Result};
1919
use clap::Parser;
20-
pub use fuzztest_options::{ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions};
20+
pub use fuzztest_options::{
21+
ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCrashOptions,
22+
};
2123
use std::env;
2224
use std::ffi::OsString;
2325
use std::path::{Path, PathBuf};
@@ -54,7 +56,15 @@ impl CargoFuzzTestOptions {
5456
fn check_centipede_binary_path_is_set(&self) -> Result<()> {
5557
anyhow::ensure!(
5658
self.centipede_binary_path.is_some(),
57-
"fuzzing mode requires `--centipede-binary-path` to be specified"
59+
"`--centipede-binary-path` needs to be specified"
60+
);
61+
Ok(())
62+
}
63+
64+
fn check_corpus_db_is_set(&self) -> Result<()> {
65+
anyhow::ensure!(
66+
self.fuzztest_options.corpus_db.is_some(),
67+
"`--corpus-db` needs to be specified"
5868
);
5969
Ok(())
6070
}
@@ -72,14 +82,18 @@ impl CargoFuzzTestOptions {
7282
self.check_centipede_binary_path_is_set()?;
7383
mode
7484
}
85+
ExecutionMode::ReplayCrash(_) => {
86+
self.check_centipede_binary_path_is_set()?;
87+
self.check_corpus_db_is_set()?;
88+
mode
89+
}
7590
ExecutionMode::SmokeTest => {
7691
if self.test_path.is_some() {
7792
self.check_centipede_binary_path_is_set()?;
78-
return Ok(ExecutionMode::Fuzz(FuzzOptions {
79-
fuzz_for: FuzzFor::Indefinitely,
80-
// TODO(the-shank): support parallel jobs
81-
jobs: None,
82-
}));
93+
ExecutionMode::Fuzz(FuzzOptions {
94+
fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely),
95+
jobs: self.fuzztest_options.jobs,
96+
})
8397
} else {
8498
mode
8599
}
@@ -227,7 +241,7 @@ impl FuzztestRunner {
227241
}
228242

229243
ExecutionMode::Fuzz(fuzz_options) => {
230-
let FuzzOptions { fuzz_for, jobs: _ } = fuzz_options;
244+
let FuzzOptions { fuzz_for, jobs } = fuzz_options;
231245
match fuzz_for {
232246
FuzzFor::Indefinitely => {
233247
cmd.env("FUZZTEST_FUZZ_FOR", "inf");
@@ -236,7 +250,13 @@ impl FuzztestRunner {
236250
cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string());
237251
}
238252
}
239-
// TODO(the-shank): support parallel jobs
253+
if let Some(jobs) = jobs {
254+
cmd.env("FUZZTEST_JOBS", jobs.to_string());
255+
}
256+
}
257+
258+
ExecutionMode::ReplayCrash(replay_options) => {
259+
cmd.env("FUZZTEST_REPLAY_ID", replay_options.replay_id);
240260
}
241261

242262
ExecutionMode::SmokeTest => {
@@ -247,13 +267,26 @@ impl FuzztestRunner {
247267
}
248268
}
249269

270+
if let Some(corpus_db) = &self.options.fuzztest_options.corpus_db {
271+
cmd.env("FUZZTEST_CORPUS_DB", corpus_db);
272+
}
273+
274+
if let Some(workdir_root) = &self.options.fuzztest_options.workdir_root {
275+
cmd.env("FUZZTEST_WORKDIR_ROOT", workdir_root);
276+
}
277+
250278
// If `--centipede-binary-path` was passed to `cargo-fuzztest`, forward it to the
251279
// child test executable via `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable so the
252280
// fuzzer runtime can locate and run Centipede during continuous fuzzing.
253281
if let Some(centipede_binary_path) = &self.options.centipede_binary_path {
254282
cmd.env("FUZZTEST_CENTIPEDE_BINARY_PATH", centipede_binary_path);
255283
}
256284

285+
if self.options.fuzztest_options.print_subprocess_log {
286+
cmd.env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true");
287+
cmd.arg("--nocapture");
288+
}
289+
257290
Ok(cmd)
258291
}
259292

@@ -377,4 +410,88 @@ mod tests {
377410
ExecutionMode::SmokeTest
378411
);
379412
}
413+
414+
#[gtest]
415+
fn test_cli_option_parsing_replay_id_success() {
416+
let parsed = CargoFuzzTestOptions::try_parse_from([
417+
"cargo-fuzztest",
418+
"--replay-id",
419+
"crash_12345",
420+
"--corpus-db",
421+
"/tmp/corpus_db",
422+
"--centipede-binary-path",
423+
"/custom/centipede",
424+
])
425+
.expect("valid replay options should parse successfully");
426+
427+
assert_eq!(parsed.fuzztest_options.replay_id.as_deref(), Some("crash_12345"));
428+
assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
429+
430+
let mode = parsed.execution_mode().expect("valid execution mode");
431+
assert_eq!(
432+
mode,
433+
ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: "crash_12345".to_string() })
434+
);
435+
}
436+
437+
#[gtest]
438+
fn test_execution_mode_replay_id_missing_corpus_db_errors() {
439+
let options = CargoFuzzTestOptions {
440+
fuzztest_options: FuzzTestOptions {
441+
replay_id: Some("crash_12345".to_string()),
442+
..Default::default()
443+
},
444+
centipede_binary_path: Some("/custom/centipede".to_string()),
445+
..Default::default()
446+
};
447+
let err = options.execution_mode().expect_err("missing corpus-db should cause error");
448+
assert!(err.to_string().contains("`--corpus-db` needs to be specified"));
449+
}
450+
451+
#[gtest]
452+
fn test_execution_mode_replay_id_missing_centipede_binary_path_errors() {
453+
let options = CargoFuzzTestOptions {
454+
fuzztest_options: FuzzTestOptions {
455+
replay_id: Some("crash_12345".to_string()),
456+
corpus_db: Some("/tmp/corpus_db".to_string()),
457+
..Default::default()
458+
},
459+
..Default::default()
460+
};
461+
let err =
462+
options.execution_mode().expect_err("missing centipede-binary-path should cause error");
463+
assert!(err.to_string().contains("`--centipede-binary-path` needs to be specified"));
464+
}
465+
466+
#[gtest]
467+
fn test_build_run_command_replay_crash() {
468+
let options = CargoFuzzTestOptions {
469+
fuzztest_options: FuzzTestOptions {
470+
replay_id: Some("crash_12345".to_string()),
471+
corpus_db: Some("/tmp/corpus_db".to_string()),
472+
..Default::default()
473+
},
474+
centipede_binary_path: Some("/custom/centipede".to_string()),
475+
..Default::default()
476+
};
477+
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
478+
let cmd =
479+
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");
480+
481+
let envs: Vec<(String, Option<String>)> = cmd
482+
.get_envs()
483+
.map(|(k, v)| {
484+
(k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
485+
})
486+
.collect();
487+
488+
assert!(envs.contains(&("FUZZTEST_REPLAY_ID".to_string(), Some("crash_12345".to_string()))));
489+
assert!(
490+
envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string())))
491+
);
492+
assert!(envs.contains(&(
493+
"FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
494+
Some("/custom/centipede".to_string())
495+
)));
496+
}
380497
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "another_sample_fuzz_crate"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
fuzztest = { path = "../../.." }
8+
googletest = "0.14.3"
9+
10+
[workspace]
11+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use fuzztest::domains::arbitrary::Arbitrary;
2+
use fuzztest::fuzztest;
3+
4+
#[fuzztest(data = Arbitrary::<i32>::default())]
5+
fn sample_fuzztest_target(data: i32) {
6+
let _ = data;
7+
}
8+
9+
#[fuzztest(data = Arbitrary::<i32>::default())]
10+
fn another_sample_fuzztest_target(data: i32) {
11+
let _ = data;
12+
}
13+
14+
#[fuzztest(data = Arbitrary::<i32>::default())]
15+
fn jobs_fuzztest_target(_data: i32) {
16+
println!("JOBS_TEST_PID: {}", std::process::id());
17+
}
18+
19+
#[fuzztest(data = Arbitrary::<u8>::default())]
20+
fn crashing_fuzztest_target(data: u8) {
21+
if data == 10 {
22+
panic!("Crashing bug found!");
23+
}
24+
}

‎rust/cargo_fuzztest/test_crates/sample_fuzz_crate/src/lib.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ fn sample_fuzztest_target(data: i32) {
1010
fn another_sample_fuzztest_target(data: i32) {
1111
let _ = data;
1212
}
13+
14+
#[fuzztest(_a = Arbitrary::<i32>::default())]
15+
fn jobs_test(_a: i32) {
16+
println!("JOBS_TEST_PID: {}", std::process::id());
17+
std::thread::sleep(std::time::Duration::from_millis(250));
18+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use cargo_fuzztest::FuzztestRunner;
2+
use std::env;
3+
use std::path::PathBuf;
4+
5+
// Helper to locate compiled sample fuzz test executables across different build environments
6+
// (Cargo tests vs Blaze/Bazel tests).
7+
pub fn get_sample_test_bin_path(crate_name: &str) -> PathBuf {
8+
let test_bin_name = format!("{crate_name}_bin");
9+
10+
// 1. Cargo test: Check if CARGO_BIN_EXE_<name> is set by Cargo when running integration tests.
11+
if let Ok(cargo_bin) = env::var(format!("CARGO_BIN_EXE_{test_bin_name}")) {
12+
return PathBuf::from(cargo_bin);
13+
}
14+
15+
// 2. Bazel/Blaze test: Fall back to TEST_SRCDIR and TEST_WORKSPACE.
16+
if let Ok(src_dir) = env::var("TEST_SRCDIR") {
17+
let test_workspace = env::var("TEST_WORKSPACE").unwrap_or_else(|_| {
18+
"_main".to_string()
19+
});
20+
21+
let relative_binary_path =
22+
format!("rust/cargo_fuzztest/{test_bin_name}");
23+
24+
let blaze_path = PathBuf::from(src_dir).join(test_workspace).join(relative_binary_path);
25+
assert!(blaze_path.exists());
26+
return blaze_path;
27+
}
28+
29+
// 3. Cargo test fallback: Query compiled test executable path via Cargo JSON compiler messages.
30+
if env::var("CARGO_MANIFEST_DIR").is_ok() {
31+
let cargo_bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
32+
let output = std::process::Command::new(cargo_bin)
33+
.args(["test", "--no-run", "--message-format=json", "--test", &test_bin_name])
34+
.output()
35+
.expect("cargo test compilation command should execute successfully");
36+
assert!(output.status.success());
37+
38+
let json_stdout = String::from_utf8_lossy(&output.stdout);
39+
if let Ok(exe) = FuzztestRunner::parse_compiler_messages(&json_stdout) {
40+
return exe;
41+
}
42+
}
43+
44+
panic!("Could not locate {test_bin_name} via CARGO_BIN_EXE, TEST_SRCDIR, or Cargo compiler messages");
45+
}

0 commit comments

Comments
 (0)