From afba201229a3df57d948d90a61b37d691f8d22fe Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Thu, 6 Aug 2026 13:28:27 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 960470283 --- rust/BUILD | 2 +- rust/cargo_fuzztest/src/lib.rs | 123 ++++++++++++++++++--- rust/cargo_fuzztest/tests/e2e_cli_test.rs | 29 +++++ rust/cargo_fuzztest/tests/runner_test.rs | 40 +++---- rust/e2e_tests/replay_test.rs | 2 + rust/e2e_tests/standalone_mode_test.rs | 27 +++++ rust/options/src/lib.rs | 126 +++++++++++++++++++--- rust/src/options.rs | 95 ++++++++++++++-- 8 files changed, 385 insertions(+), 59 deletions(-) diff --git a/rust/BUILD b/rust/BUILD index abbffd102..cba0a3bba 100644 --- a/rust/BUILD +++ b/rust/BUILD @@ -28,7 +28,7 @@ rust_library( "@com_google_fuzztest//rust/fuzztest_macro:fuzztest_macro", ], rustc_flags = ["-Zallow-features=cfg_sanitize"], - visibility = ["__subpackages__"], + visibility = ["@com_google_fuzztest//rust:__subpackages__"], deps = [ "@com_google_fuzztest//rust/coverage", "@com_google_fuzztest//rust/engine", diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index e2af0905d..bdfa67fc7 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -100,6 +100,7 @@ impl CargoFuzzTestOptions { .fuzz_for .unwrap_or(RunDuration::Indefinitely), jobs: self.fuzztest_options.jobs, + continue_after_crash: self.fuzztest_options.continue_after_crash, }) } else { mode @@ -248,11 +249,14 @@ impl FuzztestRunner { } ExecutionMode::Fuzz(fuzz_options) => { - let FuzzOptions { fuzz_for, jobs } = fuzz_options; + let FuzzOptions { fuzz_for, jobs, continue_after_crash } = fuzz_options; cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string()); if let Some(jobs) = jobs { cmd.env("FUZZTEST_JOBS", jobs.to_string()); } + if continue_after_crash { + cmd.env("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); + } } ExecutionMode::ReplayCrash(replay_options) => { @@ -273,6 +277,9 @@ impl FuzztestRunner { TimeBudgetType::Total => "total", }; cmd.env("FUZZTEST_TIME_BUDGET_TYPE", time_budget_str); + if replay_corpus_options.continue_after_crash { + cmd.env("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); + } } ExecutionMode::ListCrashIds(list_crash_ids_options) => { @@ -447,7 +454,7 @@ mod tests { .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")); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db"))); let mode = parsed.execution_mode().expect("valid execution mode"); assert_eq!( @@ -475,7 +482,7 @@ mod tests { let options = CargoFuzzTestOptions { fuzztest_options: FuzzTestOptions { replay_id: Some("crash_12345".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, ..Default::default() @@ -490,7 +497,7 @@ mod tests { let options = CargoFuzzTestOptions { fuzztest_options: FuzzTestOptions { replay_id: Some("crash_12345".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, centipede_binary_path: Some("/custom/centipede".to_string()), @@ -531,7 +538,7 @@ mod tests { .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")); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db"))); let mode = parsed.execution_mode().expect("valid execution mode"); assert_eq!(mode, ExecutionMode::ReplayAllCrashes); @@ -542,7 +549,7 @@ mod tests { let options = CargoFuzzTestOptions { fuzztest_options: FuzzTestOptions { replay_findings: true, - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, ..Default::default() @@ -557,7 +564,7 @@ mod tests { let options = CargoFuzzTestOptions { fuzztest_options: FuzzTestOptions { replay_findings: true, - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, centipede_binary_path: Some("/custom/centipede".to_string()), @@ -600,7 +607,7 @@ mod tests { assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some("10s".parse().unwrap())); assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest); - assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db")); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db"))); let mode = parsed.execution_mode().expect("valid execution mode"); assert_eq!( @@ -609,6 +616,7 @@ mod tests { replay_corpus_for: "10s".parse().expect("valid duration string"), time_budget_type: TimeBudgetType::PerTest, jobs: None, + continue_after_crash: false, }) ); } @@ -628,7 +636,7 @@ mod tests { assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest); - assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db")); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db"))); let mode = parsed.execution_mode().expect("valid execution mode"); assert_eq!( @@ -637,6 +645,7 @@ mod tests { replay_corpus_for: RunDuration::Indefinitely, time_budget_type: TimeBudgetType::PerTest, jobs: None, + continue_after_crash: false, }) ); } @@ -662,6 +671,7 @@ mod tests { replay_corpus_for: RunDuration::Indefinitely, time_budget_type: TimeBudgetType::PerTest, jobs: None, + continue_after_crash: false, }) ); } @@ -691,6 +701,7 @@ mod tests { replay_corpus_for: "10s".parse().unwrap(), time_budget_type: TimeBudgetType::Total, jobs: None, + continue_after_crash: false, }) ); } @@ -700,7 +711,7 @@ mod tests { let options = CargoFuzzTestOptions { fuzztest_options: FuzzTestOptions { replay_corpus_for: Some("10s".parse().unwrap()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, ..Default::default() @@ -716,7 +727,7 @@ mod tests { fuzztest_options: FuzzTestOptions { replay_corpus_for: Some("10s".parse().unwrap()), time_budget_type: TimeBudgetType::Total, - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, centipede_binary_path: Some("/custom/centipede".to_string()), @@ -765,7 +776,7 @@ mod tests { parsed.fuzztest_options.list_crash_ids_file.as_deref(), Some("/tmp/crashes.txt") ); - assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db")); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db"))); let mode = parsed.execution_mode().expect("valid execution mode"); assert_eq!( @@ -797,7 +808,7 @@ mod tests { fuzztest_options: FuzzTestOptions { list_crash_ids: true, list_crash_ids_file: Some("/tmp/crashes.txt".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, ..Default::default() @@ -813,7 +824,7 @@ mod tests { fuzztest_options: FuzzTestOptions { list_crash_ids: true, list_crash_ids_file: Some("/tmp/crashes.txt".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, centipede_binary_path: Some("/custom/centipede".to_string()), @@ -850,7 +861,7 @@ mod tests { fuzztest_options: FuzzTestOptions { replay_corpus_for: Some(RunDuration::Indefinitely), time_budget_type: TimeBudgetType::Total, - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }, centipede_binary_path: Some("/custom/centipede".to_string()), @@ -879,4 +890,86 @@ mod tests { Some("/custom/centipede".to_string()) ))); } + + #[gtest] + fn test_cli_option_parsing_continue_after_crash_flag() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--fuzz-for", + "5s", + "--continue-after-crash", + "--centipede-binary-path", + "/custom/centipede", + ]) + .unwrap(); + + assert!(parsed.fuzztest_options.continue_after_crash); + let mode = parsed.execution_mode().unwrap(); + let expected_duration = "5s".parse().unwrap(); + assert_eq!( + mode, + ExecutionMode::Fuzz(FuzzOptions { + fuzz_for: RunDuration::Fixed(expected_duration), + jobs: None, + continue_after_crash: true, + }) + ); + } + + #[gtest] + fn test_build_run_command_fuzz_with_continue_after_crash() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + fuzz_for: Some("5s".parse().unwrap()), + continue_after_crash: true, + ..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)> = 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_CONTINUE_AFTER_CRASH".to_string(), Some("true".to_string()))) + ); + assert!(envs.contains(&("FUZZTEST_FUZZ_FOR".to_string(), Some("5s".to_string())))); + } + + #[gtest] + fn test_build_run_command_replay_corpus_with_continue_after_crash() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some("10s".parse().expect("valid duration")), + corpus_db: Some("/tmp/corpus_db".into()), + continue_after_crash: true, + ..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)> = 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_CONTINUE_AFTER_CRASH".to_string(), Some("true".to_string()))) + ); + assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("10s".to_string())))); + } } diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index 04ed647ab..b5a871739 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -165,6 +165,7 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") + .arg("--continue-after-crash") .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") .arg("--centipede-binary-path") .arg(¢ipede_bin) @@ -248,6 +249,7 @@ fn test_cargo_fuzztest_e2e_replay_all_crashes() { let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") + .arg("--continue-after-crash") .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") .arg("--centipede-binary-path") .arg(¢ipede_bin) @@ -412,6 +414,7 @@ fn test_cargo_fuzztest_e2e_list_crash_ids() { let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") + .arg("--continue-after-crash") .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") .arg("--centipede-binary-path") .arg(¢ipede_bin) @@ -448,3 +451,29 @@ fn test_cargo_fuzztest_e2e_list_crash_ids() { // good check here? expect_true!(!crash_ids.is_empty()); } + +#[gtest] +fn test_cargo_fuzztest_e2e_continue_after_crash() { + let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); + + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--fuzz-for=3s") + .arg("--continue-after-crash") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true"); + + let output = cmd.output().expect("Failed to run cargo-fuzztest with continue-after-crash"); + let stderr_str = String::from_utf8_lossy(&output.stderr); + + expect_true!(output.status.success()); + expect_true!(stderr_str.contains("Property function ran but crashed.")); + expect_true!(stderr_str.contains("Crashing bug found!")); +} diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index 14bd430b0..2a224519c 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -33,7 +33,7 @@ fn test_runner_build_run_command_with_target() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let args: Vec = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect(); @@ -52,7 +52,7 @@ fn test_runner_build_run_command_with_duration() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -72,7 +72,7 @@ fn test_runner_build_run_command_with_indefinitely() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -89,7 +89,7 @@ fn test_runner_build_run_command_with_centipede_binary_path() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -115,7 +115,7 @@ fn test_runner_build_run_command_with_jobs() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -131,7 +131,7 @@ fn test_runner_build_run_command_with_jobs_only() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -147,7 +147,7 @@ fn test_runner_build_run_command_with_replay_id() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { replay_id: Some("crash_12345".to_string()), - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -155,7 +155,7 @@ fn test_runner_build_run_command_with_replay_id() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -194,7 +194,7 @@ fn test_execution_mode_replay_id_missing_corpus_db_errors() { fn test_execution_mode_replay_id_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { replay_id: Some("crash_12345".to_string()), - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; @@ -209,7 +209,7 @@ fn test_runner_build_run_command_with_replay_findings() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { replay_findings: true, - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -217,7 +217,7 @@ fn test_runner_build_run_command_with_replay_findings() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -239,7 +239,7 @@ fn test_runner_build_run_command_with_replay_findings() { fn test_execution_mode_replay_findings_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { replay_findings: true, - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; @@ -255,7 +255,7 @@ fn test_runner_build_run_command_with_replay_corpus() { let fuzztest_options = FuzzTestOptions { replay_corpus_for: Some("10s".parse().expect("valid duration")), time_budget_type: TimeBudgetType::Total, - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -263,7 +263,7 @@ fn test_runner_build_run_command_with_replay_corpus() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -292,7 +292,7 @@ fn test_runner_build_run_command_with_replay_corpus_indefinitely() { let fuzztest_options = FuzzTestOptions { replay_corpus_for: Some(RunDuration::Indefinitely), time_budget_type: TimeBudgetType::Total, - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -300,7 +300,7 @@ fn test_runner_build_run_command_with_replay_corpus_indefinitely() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -327,7 +327,7 @@ fn test_runner_build_run_command_with_replay_corpus_indefinitely() { fn test_execution_mode_replay_corpus_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { replay_corpus_for: Some("10s".parse().expect("valid duration")), - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; @@ -359,7 +359,7 @@ fn test_runner_build_run_command_with_list_crash_ids() { let fuzztest_options = FuzzTestOptions { list_crash_ids: true, list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -367,7 +367,7 @@ fn test_runner_build_run_command_with_list_crash_ids() { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() }; - let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); let cmd = runner.build_run_command(&binary_path).expect("valid run command"); let envs: Vec<(String, Option)> = cmd @@ -412,7 +412,7 @@ fn test_execution_mode_list_crash_ids_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { list_crash_ids: true, list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), - corpus_db: Some("/custom/path/to/corpus_db".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".into()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; diff --git a/rust/e2e_tests/replay_test.rs b/rust/e2e_tests/replay_test.rs index d14d7722c..80c37ed89 100644 --- a/rust/e2e_tests/replay_test.rs +++ b/rust/e2e_tests/replay_test.rs @@ -48,6 +48,7 @@ fn replay_by_id_reproduces_panic(fixture: &EnvVars) { .arg(test_name) .arg("--exact") .env("FUZZTEST_FUZZ_FOR", "15s") + .env("FUZZTEST_CONTINUE_AFTER_CRASH", "true") .env("FUZZTEST_CORPUS_DB", &db_dir) .env("FUZZTEST_WORKDIR_ROOT", &workdir_root_dir) .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) @@ -127,6 +128,7 @@ fn replay_all_reproduces_all_failures(fixture: &EnvVars) { .arg(test_name) .arg("--exact") .env("FUZZTEST_FUZZ_FOR", "15s") + .env("FUZZTEST_CONTINUE_AFTER_CRASH", "true") .env("FUZZTEST_CORPUS_DB", &db_dir) .env("FUZZTEST_WORKDIR_ROOT", &workdir_root_dir) .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) diff --git a/rust/e2e_tests/standalone_mode_test.rs b/rust/e2e_tests/standalone_mode_test.rs index 53adb4818..55022c582 100644 --- a/rust/e2e_tests/standalone_mode_test.rs +++ b/rust/e2e_tests/standalone_mode_test.rs @@ -95,12 +95,39 @@ fn standalone_mode_handles_worker_crash(fixture: &EnvVars) { let output = process.wait_with_output().expect("Should terminate"); let stderr = String::from_utf8_lossy(&output.stderr); + // By default continue_after_crash is false, so finding a crash causes test failure. + expect_false!(output.status.success()); expect_that!(stderr, matchers::contains_substring("Property function ran but crashed.")); expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: Unwinding panic")); // Centipede prefixes logs from the crashing worker with "CRASH LOG: ". expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!")); } +#[gtest] +fn standalone_mode_continues_after_crash_when_enabled(fixture: &EnvVars) { + let test_name = "__fuzztest_mod__find_bug_fuzz_test::find_bug_fuzz_test"; + + let process = Command::new(&fixture.target_binary_path) + .arg(test_name) + .env("FUZZTEST_FUZZ_FOR", "3s") + .env("FUZZTEST_CONTINUE_AFTER_CRASH", "true") + .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") + .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) + .env("RUST_TEST_NOCAPTURE", "1") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn binary"); + + let output = process.wait_with_output().expect("Should terminate"); + let stderr = String::from_utf8_lossy(&output.stderr); + + // With continue_after_crash=true, the fuzz test runs for the full time limit and exits cleanly. + expect_true!(output.status.success()); + expect_that!(stderr, matchers::contains_substring("Property function ran but crashed.")); + expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!")); +} + #[gtest] fn standalone_mode_spawns_parallel_jobs(fixture: &EnvVars) { let target_binary_path = diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index 93f9cc40c..6f6eabc8d 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -20,11 +20,31 @@ use anyhow::Context; use clap::{Parser, ValueEnum}; +use std::env; use std::fmt; use std::fmt::Display; use std::fmt::Formatter; +use std::path::{Path, PathBuf}; use std::str::FromStr; +/// If a relative path is provided while executing inside a Bazel/Blaze test sandbox +/// (detected via the `TEST_SRCDIR` environment variable), the path is automatically +/// resolved relative to the runfiles root directory. +fn parse_corpus_db_path(raw: &str) -> Result { + if raw.is_empty() { + return Err("corpus_db path cannot be empty".to_string()); + } + + let path = PathBuf::from(raw); + if path.is_absolute() { + Ok(path) + } else if let Ok(test_src_dir) = env::var("TEST_SRCDIR") { + Ok(Path::new(&test_src_dir).join(path)) + } else { + Ok(path) + } +} + /// Time budget calculation type for replay corpus mode. #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TimeBudgetType { @@ -100,8 +120,21 @@ pub struct FuzzTestOptions { /// /// If set to non-empty, updates/queries the corpus database that contains coverage, /// regression, and crashing inputs for each test binary and fuzz test. - #[arg(env = "FUZZTEST_CORPUS_DB", long)] - pub corpus_db: Option, + #[arg(env = "FUZZTEST_CORPUS_DB", long, value_parser = parse_corpus_db_path)] + pub corpus_db: Option, + + /// Controls the fuzzing and corpus replaying behavior when a crashing input is found. + /// + /// If set to false (default), the test execution stops upon finding the first crashing input, + /// and the test fails. If set to true, the execution logs any crash inputs found and continues + /// until reaching the time limit or manually stopped. + /// + /// Note that this does not affect crash replaying: + /// - Replaying a single crash (`--replay-id`) always stops immediately when a crash occurs. + /// - Replaying all crashes (`--replay-findings`) always continues replaying all remaining + /// crashing inputs regardless of this flag. + #[arg(env = "FUZZTEST_CONTINUE_AFTER_CRASH", long)] + pub continue_after_crash: bool, } /// Strongly-typed domain execution mode for test runs. @@ -152,6 +185,7 @@ impl ExecutionMode { replay_corpus_for, time_budget_type: options.time_budget_type, jobs: options.jobs, + continue_after_crash: options.continue_after_crash, }); } @@ -165,7 +199,11 @@ impl ExecutionMode { // Continuous fuzzing mode is selected if an explicit duration/budget (`fuzz_for`) is specified. if let Some(fuzz_for) = &options.fuzz_for { - return ExecutionMode::Fuzz(FuzzOptions { fuzz_for: *fuzz_for, jobs: options.jobs }); + return ExecutionMode::Fuzz(FuzzOptions { + fuzz_for: *fuzz_for, + jobs: options.jobs, + continue_after_crash: options.continue_after_crash, + }); } ExecutionMode::SmokeTest @@ -180,6 +218,8 @@ pub struct FuzzOptions { /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it /// will use its own default value. pub jobs: Option, + + pub continue_after_crash: bool, } /// The duration or limit for fuzzing or replaying corpus. @@ -231,6 +271,7 @@ pub struct ReplayCorpusOptions { /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it /// will use its own default value. pub jobs: Option, + pub continue_after_crash: bool, } /// Mode-specific options for listing crash IDs from the database. @@ -316,7 +357,7 @@ mod tests { let options = result.expect("parsing should succeed when both replay_id and corpus_db are present"); expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123"))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); } #[gtest] @@ -356,6 +397,7 @@ mod tests { eq(&ExecutionMode::Fuzz(FuzzOptions { fuzz_for: RunDuration::Fixed(expected_duration), jobs: Some(4), + continue_after_crash: false, })) ); @@ -385,6 +427,7 @@ mod tests { replay_corpus_for: expected_duration, time_budget_type: TimeBudgetType::PerTest, jobs: Some(4), + continue_after_crash: false, })) ); @@ -396,6 +439,63 @@ mod tests { } } + #[gtest] + fn test_continue_after_crash_with_fuzz_for_parsing_env() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_FUZZ_FOR", "5s"); + std::env::set_var("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); + } + + let options = FuzzTestOptions::parse_from(std::iter::empty::()); + expect_true!(options.continue_after_crash); + let expected_duration = "5s".parse().expect("valid duration"); + expect_that!( + ExecutionMode::from_fuzztest_options(&options), + eq(&ExecutionMode::Fuzz(FuzzOptions { + fuzz_for: RunDuration::Fixed(expected_duration), + jobs: None, + continue_after_crash: true, + })) + ); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_FUZZ_FOR"); + std::env::remove_var("FUZZTEST_CONTINUE_AFTER_CRASH"); + } + } + + #[gtest] + fn test_continue_after_crash_with_replay_corpus_parsing_env() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + std::env::set_var("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); + } + + let options = FuzzTestOptions::parse_from(std::iter::empty::()); + expect_true!(options.continue_after_crash); + let expected_duration = "10s".parse().expect("valid duration string"); + expect_that!( + ExecutionMode::from_fuzztest_options(&options), + eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: expected_duration, + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + continue_after_crash: true, + })) + ); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + std::env::remove_var("FUZZTEST_CONTINUE_AFTER_CRASH"); + } + } + #[gtest] fn test_replay_findings_requires_corpus_db() { // SAFETY: Testing environment parsing in single-threaded context. @@ -419,8 +519,8 @@ mod tests { fn test_replay_findings_with_corpus_db_succeeds() { // SAFETY: Testing environment parsing in single-threaded context. unsafe { - std::env::set_var("FUZZTEST_REPLAY_FINDINGS", "true"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + env::set_var("FUZZTEST_REPLAY_FINDINGS", "true"); + env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); } let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); @@ -434,7 +534,7 @@ mod tests { let options = result .expect("parsing should succeed when both replay_findings and corpus_db are present"); expect_that!(options.replay_findings, eq(true)); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); } #[gtest] @@ -478,7 +578,7 @@ mod tests { options.replay_corpus_for, eq(Some("10s".parse().expect("valid duration string"))) ); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); expect_that!(options.time_budget_type, eq(TimeBudgetType::PerTest)); } @@ -502,7 +602,7 @@ mod tests { "parsing should succeed when replay_corpus_for is inf and corpus_db is present", ); expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); } #[gtest] @@ -525,7 +625,7 @@ mod tests { "parsing should succeed when replay_corpus_for is infinity and corpus_db is present", ); expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); } #[gtest] @@ -551,7 +651,7 @@ mod tests { options.replay_corpus_for, eq(Some("10s".parse().expect("valid duration string"))) ); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); } @@ -575,7 +675,7 @@ mod tests { let options = result.expect("parsing should succeed with total time budget type and inf"); expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); } @@ -665,7 +765,7 @@ mod tests { ); expect_that!(options.list_crash_ids, eq(true)); expect_that!(options.list_crash_ids_file.as_deref(), eq(Some("/tmp/crashes.txt"))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); let mode = ExecutionMode::from_fuzztest_options(&options); expect_that!( diff --git a/rust/src/options.rs b/rust/src/options.rs index 41abaf5a4..9f4971827 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -229,6 +229,9 @@ impl CentipedeArgs { if let Some(jobs) = &fuzz_opts.jobs { add_arg(format!("--j={jobs}"))?; } + if !fuzz_opts.continue_after_crash { + add_arg("--exit_on_crash".to_string())?; + } } ExecutionMode::ReplayCrash(replay_opts) => { add_arg("--replay_crash".to_string())?; @@ -270,6 +273,9 @@ impl CentipedeArgs { if let Some(jobs) = &replay_corpus_opts.jobs { add_arg(format!("--j={jobs}"))?; } + if !replay_corpus_opts.continue_after_crash { + add_arg("--exit_on_crash".to_string())?; + } } ExecutionMode::ListCrashIds(list_opts) => { add_arg("--list_crash_ids=true".to_string())?; @@ -340,11 +346,19 @@ fn get_corpusdb_and_workdir_root_and_workdir( if let Some(corpus_db) = corpus_db { if let Some(workdir_root) = workdir_root { - Ok((Some(corpus_db), Some(workdir_root), None)) + Ok(( + Some(corpus_db.to_str().context("converting corpus_db to str")?), + Some(workdir_root), + None, + )) } else { let temp_dir = TempDir::new().context("while attempting to create temporary working directory")?; - Ok((Some(corpus_db), None, Some(temp_dir))) + Ok(( + Some(corpus_db.to_str().context("converting corpus_db to str")?), + None, + Some(temp_dir), + )) } } else { if workdir_root.is_some() { @@ -549,7 +563,7 @@ mod tests { fn test_determine_execution_action_replay_crash_id() { let options = FuzzTestOptions { replay_id: Some("my_crash_123".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); @@ -572,7 +586,7 @@ mod tests { fn test_determine_execution_action_replay_findings() { let options = FuzzTestOptions { replay_findings: true, - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); @@ -730,7 +744,7 @@ mod tests { #[gtest] fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_and_workdir_root() -> Result<()> { let options = FuzzTestOptions { - corpus_db: Some("/tmp/db".to_string()), + corpus_db: Some("/tmp/db".into()), workdir_root: Some("/tmp/root".to_string()), ..Default::default() }; @@ -744,8 +758,7 @@ mod tests { #[gtest] fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_only() -> Result<()> { - let options = - FuzzTestOptions { corpus_db: Some("/tmp/db".to_string()), ..Default::default() }; + let options = FuzzTestOptions { corpus_db: Some("/tmp/db".into()), ..Default::default() }; let (corpus_db, workdir_root, workdir) = get_corpusdb_and_workdir_root_and_workdir(&options).or_fail()?; expect_that!(corpus_db, eq(Some("/tmp/db"))); @@ -792,7 +805,7 @@ mod tests { let options = result.expect("parsing should succeed when both replay_id and corpus_db are present"); expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123"))); - expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); } #[gtest] @@ -826,7 +839,7 @@ mod tests { let duration = "1s".parse().expect("fixed test string should parse as duration"); let options = FuzzTestOptions { fuzz_for: Some(duration), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); @@ -851,7 +864,7 @@ mod tests { let options = FuzzTestOptions { list_crash_ids: true, list_crash_ids_file: Some("/tmp/custom_crashes.txt".to_string()), - corpus_db: Some("/tmp/corpus_db".to_string()), + corpus_db: Some("/tmp/corpus_db".into()), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); @@ -869,4 +882,66 @@ mod tests { expect_true!(args_str.iter().any(|s| s.starts_with("--workdir="))); expect_true!(args_str.contains(&"--test_name=my_mod.my_test")); } + + #[gtest] + fn test_determine_execution_action_fuzz_continue_after_crash_false() { + let options = FuzzTestOptions { + fuzz_for: Some("5s".parse().unwrap()), + continue_after_crash: false, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + let args_str: Vec<&str> = args._c_strings.iter().map(|s| s.to_str().unwrap()).collect(); + expect_true!(args_str.contains(&"--exit_on_crash")); + } + + #[gtest] + fn test_determine_execution_action_fuzz_continue_after_crash_true() { + let options = FuzzTestOptions { + fuzz_for: Some("5s".parse().unwrap()), + continue_after_crash: true, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + let args_str: Vec<&str> = args._c_strings.iter().map(|s| s.to_str().unwrap()).collect(); + expect_false!(args_str.contains(&"--exit_on_crash")); + } + + #[gtest] + fn test_determine_execution_action_replay_corpus_continue_after_crash_false() { + let options = FuzzTestOptions { + replay_corpus_for: Some("5s".parse().unwrap()), + continue_after_crash: false, + corpus_db: Some("/tmp/corpus_db".into()), + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + let args_str: Vec<&str> = args._c_strings.iter().map(|s| s.to_str().unwrap()).collect(); + expect_true!(args_str.contains(&"--exit_on_crash")); + } + + #[gtest] + fn test_determine_execution_action_replay_corpus_continue_after_crash_true() { + let options = FuzzTestOptions { + replay_corpus_for: Some("5s".parse().unwrap()), + continue_after_crash: true, + corpus_db: Some("/tmp/corpus_db".into()), + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + let args_str: Vec<&str> = args._c_strings.iter().map(|s| s.to_str().unwrap()).collect(); + expect_false!(args_str.contains(&"--exit_on_crash")); + } }