diff --git a/rust/BUILD b/rust/BUILD index eb43c1107..abbffd102 100644 --- a/rust/BUILD +++ b/rust/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -57,3 +57,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "fuzztest_clippy", + deps = [ + ":fuzztest", + ], +) diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index 78b6942a7..e2af0905d 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -18,7 +18,8 @@ use anyhow::{Context, Result}; use clap::Parser; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCrashOptions, + ExecutionMode, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, + ReplayCrashOptions, RunDuration, TimeBudgetType, }; use std::env; use std::ffi::OsString; @@ -82,7 +83,10 @@ impl CargoFuzzTestOptions { self.check_centipede_binary_path_is_set()?; mode } - ExecutionMode::ReplayCrash(_) => { + ExecutionMode::ReplayCorpus(_) + | ExecutionMode::ReplayAllCrashes + | ExecutionMode::ReplayCrash(_) + | ExecutionMode::ListCrashIds(_) => { self.check_centipede_binary_path_is_set()?; self.check_corpus_db_is_set()?; mode @@ -91,7 +95,10 @@ impl CargoFuzzTestOptions { if self.test_path.is_some() { self.check_centipede_binary_path_is_set()?; ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely), + fuzz_for: self + .fuzztest_options + .fuzz_for + .unwrap_or(RunDuration::Indefinitely), jobs: self.fuzztest_options.jobs, }) } else { @@ -242,14 +249,7 @@ impl FuzztestRunner { ExecutionMode::Fuzz(fuzz_options) => { let FuzzOptions { fuzz_for, jobs } = fuzz_options; - match fuzz_for { - FuzzFor::Indefinitely => { - cmd.env("FUZZTEST_FUZZ_FOR", "inf"); - } - FuzzFor::Duration(duration) => { - cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string()); - } - } + cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string()); if let Some(jobs) = jobs { cmd.env("FUZZTEST_JOBS", jobs.to_string()); } @@ -259,12 +259,33 @@ impl FuzztestRunner { cmd.env("FUZZTEST_REPLAY_ID", replay_options.replay_id); } + ExecutionMode::ReplayAllCrashes => { + cmd.env("FUZZTEST_REPLAY_FINDINGS", "true"); + } + + ExecutionMode::ReplayCorpus(replay_corpus_options) => { + cmd.env( + "FUZZTEST_REPLAY_CORPUS_FOR", + replay_corpus_options.replay_corpus_for.to_string(), + ); + let time_budget_str = match replay_corpus_options.time_budget_type { + TimeBudgetType::PerTest => "per-test", + TimeBudgetType::Total => "total", + }; + cmd.env("FUZZTEST_TIME_BUDGET_TYPE", time_budget_str); + } + + ExecutionMode::ListCrashIds(list_crash_ids_options) => { + cmd.env("FUZZTEST_LIST_CRASH_IDS", "true"); + cmd.env( + "FUZZTEST_LIST_CRASH_IDS_FILE", + &list_crash_ids_options.list_crash_ids_file, + ); + } + ExecutionMode::SmokeTest => { // nothing to be done } - _ => { - // TODO(the-shank): add support for other modes. - } } if let Some(corpus_db) = &self.options.fuzztest_options.corpus_db { @@ -328,6 +349,7 @@ impl FuzztestRunner { mod tests { use super::*; use googletest::prelude::*; + use std::collections::HashMap; #[gtest] fn test_parse_host_triple_valid() { @@ -478,6 +500,366 @@ mod tests { let cmd = runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command"); + let envs: HashMap> = 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> = 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") + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_success() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "10s", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for options should parse successfully"); + + 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")); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: "10s".parse().expect("valid duration string"), + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_inf() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "inf", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for inf should parse successfully"); + + 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")); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_infinity() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "infinity", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for infinity should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_with_time_budget_type() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "10s", + "--time-budget-type", + "total", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid options with time-budget-type should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some("10s".parse().unwrap())); + assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::Total); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: "10s".parse().unwrap(), + time_budget_type: TimeBudgetType::Total, + jobs: None, + }) + ); + } + + #[gtest] + fn test_execution_mode_replay_corpus_missing_centipede_binary_path_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some("10s".parse().unwrap()), + 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_corpus() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some("10s".parse().unwrap()), + time_budget_type: TimeBudgetType::Total, + 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)> = 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_CORPUS_FOR".to_string(), Some("10s".to_string())))); + assert!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".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()) + ))); + } + + #[gtest] + fn test_cli_option_parsing_list_crash_ids_success() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--list-crash-ids", + "--list-crash-ids-file", + "/tmp/crashes.txt", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid list-crash-ids options should parse successfully"); + + assert!(parsed.fuzztest_options.list_crash_ids); + assert_eq!( + 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")); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ListCrashIds(ListCrashIdsOptions { + list_crash_ids_file: "/tmp/crashes.txt".to_string(), + }) + ); + } + + #[gtest] + fn test_execution_mode_list_crash_ids_missing_corpus_db_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/tmp/crashes.txt".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_list_crash_ids_missing_centipede_binary_path_errors() { + let options = CargoFuzzTestOptions { + 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()), + ..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_list_crash_ids() { + let options = CargoFuzzTestOptions { + 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()), + ..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_LIST_CRASH_IDS".to_string(), Some("true".to_string())))); + assert!(envs.contains(&( + "FUZZTEST_LIST_CRASH_IDS_FILE".to_string(), + Some("/tmp/crashes.txt".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()) + ))); + } + + #[gtest] + fn test_build_run_command_replay_corpus_indefinite() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + 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)> = cmd .get_envs() .map(|(k, v)| { @@ -485,7 +867,10 @@ mod tests { }) .collect(); - assert!(envs.contains(&("FUZZTEST_REPLAY_ID".to_string(), Some("crash_12345".to_string())))); + assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string())))); + assert!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); assert!( envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string()))) ); diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index 221a21d8d..04ed647ab 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -144,9 +144,6 @@ fn test_cargo_fuzztest_e2e_parallel_jobs() { #[gtest] fn test_cargo_fuzztest_e2e_replay_by_id() { - let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") - .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); - let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); @@ -159,34 +156,16 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { TempDir::new().expect("Failed to create temporary workdir_root directory"); fs::create_dir_all(&workdir_root_dir).expect("Failed to workdir_root directory"); - let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; - let normalized_test_name = test_target.replace("::", "."); - - // 1. Retrieve the target binary path. - let host_triple = cargo_fuzztest::get_host_target_triple() - .expect("Failed to get host target triple for compilation"); - let runner = cargo_fuzztest::FuzztestRunner::new( - host_triple, - cargo_fuzztest::CargoFuzzTestOptions::default(), - ); - let mut compile_cmd = runner.build_compile_command(); - compile_cmd.current_dir(&sample_crate_path).env("CARGO_TARGET_DIR", temp_target_dir.path()); - - let compile_output = compile_cmd.output().expect("Failed to execute cargo compilation command"); - assert!(compile_output.status.success()); - let json_stdout = String::from_utf8(compile_output.stdout) - .expect("Cargo compilation stdout must be valid UTF-8"); - let target_binary_path = cargo_fuzztest::FuzztestRunner::parse_compiler_messages(&json_stdout) - .expect("Failed to parse target binary path from cargo JSON output"); + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); - let target_binary_str = target_binary_path.to_str().expect("Valid binary path string"); - let binary_id = target_binary_str.strip_prefix('/').unwrap_or(target_binary_str); + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; - // 2. Run Centipede to fuzz the target and populate the corpus database. + // 1. Run Centipede via cargo-fuzztest to fuzz the target and populate the corpus database. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") - .env_remove("CENTIPEDE_BINARY_PATH") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") .arg("--centipede-binary-path") .arg(¢ipede_bin) .arg("--corpus-db") @@ -197,20 +176,22 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); assert!(output.status.success()); - // 3. Get list of crash ids + // 2. Get list of crash ids via cargo-fuzztest --list-crash-ids let list_temp_dir = TempDir::new().expect("Failed to create temporary list directory"); let crash_ids_file = list_temp_dir.path().join("crash_ids.txt"); - let list_args = [ - format!("--binary={}", target_binary_path.display()), - format!("--fuzztest_binary_identifier={}", binary_id), - format!("--test_name={}", normalized_test_name), - format!("--fuzztest_corpus_database={}", temp_db_dir.path().display()), - "--list_crash_ids=1".to_string(), - format!("--list_crash_ids_file={}", crash_ids_file.display()), - ]; - let list_args_refs: Vec<&str> = list_args.iter().map(|s| s.as_str()).collect(); - run_centipede_with_args_expect_termination(¢ipede_bin, &list_args_refs); + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--list-crash-ids") + .arg("--list-crash-ids-file") + .arg(&crash_ids_file) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest to list crash IDs"); + assert!(output.status.success()); let crash_ids_contents = fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); @@ -220,7 +201,7 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { expect_true!(!crash_ids.is_empty()); let crash_id = crash_ids[0]; - // 4. Run cargo-fuzztest CLI with --replay-id to verify it replays the crash. + // 3. Run cargo-fuzztest CLI with --replay-id to verify it replays the crash. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--replay-id") @@ -244,32 +225,226 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { ); } -fn run_centipede_with_args_expect_termination(centipede_bin: &str, args: &[&str]) -> String { - // Disable interference from Bazel environment variables. - let env_diff = [ - "-TEST_DIAGNOSTICS_OUTPUT_DIR", - "-TEST_INFRASTRUCTURE_FAILURE_FILE", - "-TEST_LOGSPLITTER_OUTPUT_FILE", - "-TEST_PREMATURE_EXIT_FILE", - "-TEST_RANDOM_SEED", - "-TEST_RUN_NUMBER", - "-TEST_SHARD_INDEX", - "-TEST_SHARD_STATUS_FILE", - "-TEST_TOTAL_SHARDS", - "-TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR", - "-TEST_UNDECLARED_OUTPUTS_DIR", - "-TEST_WARNINGS_OUTPUT_FILE", - "-GTEST_OUTPUT", - "-XML_OUTPUT_FILE", - ]; - let process = Command::new(centipede_bin) - .arg("--populate_binary_info=0") - .arg("--fork_server=0") - .arg("--persistent_mode=0") - .arg(format!("--env_diff_for_binaries={}", env_diff.join(","))) - .args(args) - .output() - .expect("Centipede should have executed"); +#[gtest] +fn test_cargo_fuzztest_e2e_replay_all_crashes() { + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be provided"); + + let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + fs::create_dir_all(&temp_target_dir).expect("Failed to create target directory"); + + let temp_db_dir = TempDir::new().expect("Failed to create temporary corpus db directory"); + fs::create_dir_all(&temp_db_dir).expect("Failed to create corpus db directory"); + + let workdir_root_dir = + TempDir::new().expect("Failed to create temporary workdir_root directory"); + fs::create_dir_all(&workdir_root_dir).expect("Failed to create workdir_root directory"); + + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; + + // 1. Run Centipede via cargo-fuzztest to fuzz the target and populate the corpus database. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--fuzz-for=5s") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()); + let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); + assert!(output.status.success()); + + // 2. Run cargo-fuzztest CLI with --replay-findings to verify it replays all crashes from corpus db. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--replay-findings") + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in replay-findings mode"); + + let stderr_str = String::from_utf8_lossy(&output.stderr); + let stdout_str = String::from_utf8_lossy(&output.stdout); + expect_true!(output.status.success()); + expect_true!( + stderr_str.contains("Crashing bug found!") || stdout_str.contains("Crashing bug found!") + ); +} + +#[gtest] +fn test_cargo_fuzztest_e2e_replay_corpus() { + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be provided"); + + let sample_crate_path = get_sample_crate_path("sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + fs::create_dir_all(&temp_target_dir).expect("Failed to create target directory"); + + let temp_db_dir = TempDir::new().expect("Failed to create temporary corpus db directory"); + fs::create_dir_all(&temp_db_dir).expect("Failed to create corpus db directory"); + + let workdir_root_dir = + TempDir::new().expect("Failed to create temporary workdir_root directory"); + fs::create_dir_all(&workdir_root_dir).expect("Failed to create workdir_root directory"); + + // 1. Run Centipede via cargo-fuzztest to fuzz and populate the corpus database. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") + .arg("--fuzz-for=3s") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()); + let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); + assert!(output.status.success()); + + // 2. Run cargo-fuzztest CLI with --replay-corpus-for to verify it replays the corpus. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") + .arg("--replay-corpus-for=2s") + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in replay-corpus mode"); + let stderr_str = String::from_utf8_lossy(&output.stderr); + let stdout_str = String::from_utf8_lossy(&output.stdout); + + expect_true!(output.status.success()); + expect_true!( + stderr_str.contains( + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 2s" + ) || stdout_str.contains( + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 2s" + ) + ); +} + +#[gtest] +fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be provided"); + + let sample_crate_path = get_sample_crate_path("sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + fs::create_dir_all(&temp_target_dir).expect("Failed to create target directory"); + + let temp_db_dir = TempDir::new().expect("Failed to create temporary corpus db directory"); + fs::create_dir_all(&temp_db_dir).expect("Failed to create corpus db directory"); + + let workdir_root_dir = + TempDir::new().expect("Failed to create temporary workdir_root directory"); + fs::create_dir_all(&workdir_root_dir).expect("Failed to create workdir_root directory"); + + // 1. Run Centipede via cargo-fuzztest to fuzz and populate the corpus database. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") + .arg("--fuzz-for=3s") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()); + let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); + assert!(output.status.success()); + + // 2. Run cargo-fuzztest CLI with --replay-corpus-for and --time-budget-type total. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") + .arg("--replay-corpus-for=4.5s") + .arg("--time-budget-type=total") + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in replay-corpus mode"); + let stderr_str = String::from_utf8_lossy(&output.stderr); + let stdout_str = String::from_utf8_lossy(&output.stdout); + + eprintln!("tmp:: stderr:\n{stderr_str}"); + eprintln!("tmp:: stdout:\n{stdout_str}"); + + expect_true!(output.status.success()); + expect_true!( + stderr_str.contains( + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" + ) || stdout_str.contains( + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" + ) + ); +} + +#[gtest] +fn test_cargo_fuzztest_e2e_list_crash_ids() { + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be provided"); + + let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + fs::create_dir_all(&temp_target_dir).expect("Failed to create target directory"); + + let temp_db_dir = TempDir::new().expect("Failed to create temporary corpus db directory"); + fs::create_dir_all(&temp_db_dir).expect("Failed to create corpus db directory"); + + let workdir_root_dir = + TempDir::new().expect("Failed to create temporary workdir_root directory"); + fs::create_dir_all(&workdir_root_dir).expect("Failed to create workdir_root directory"); + + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; + + // 1. Run Centipede via cargo-fuzztest to fuzz the target and populate the corpus database. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--fuzz-for=5s") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()); + let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); + assert!(output.status.success()); + + // 2. Run cargo-fuzztest CLI with --list-crash-ids and --list-crash-ids-file to list crashes from corpus db. + let list_temp_dir = TempDir::new().expect("Failed to create temporary list directory"); + let crash_ids_file = list_temp_dir.path().join("crash_ids.txt"); - String::from_utf8_lossy(&process.stderr).to_string() + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--list-crash-ids") + .arg("--list-crash-ids-file") + .arg(&crash_ids_file) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in list-crash-ids mode"); + assert!(output.status.success()); + + let crash_ids_contents = + fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); + let crash_ids: Vec<&str> = + crash_ids_contents.lines().filter(|line| !line.trim().is_empty()).collect(); + + // DISCUSS: We probably should be checking for something more specific, but what would be a + // good check here? + expect_true!(!crash_ids.is_empty()); } diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index b45852c3e..14bd430b0 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -2,7 +2,7 @@ mod common; use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner}; use common::get_sample_test_bin_path; -use fuzztest_options::{FuzzFor, FuzzTestOptions}; +use fuzztest_options::{FuzzTestOptions, RunDuration, TimeBudgetType}; use googletest::prelude::*; #[gtest] @@ -45,10 +45,8 @@ fn test_runner_build_run_command_with_target() { #[gtest] fn test_runner_build_run_command_with_duration() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); - let fuzztest_options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())), - ..Default::default() - }; + let fuzztest_options = + FuzzTestOptions { fuzz_for: Some("5s".parse().unwrap()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -68,7 +66,7 @@ fn test_runner_build_run_command_with_duration() { fn test_runner_build_run_command_with_indefinitely() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -109,7 +107,7 @@ fn test_runner_build_run_command_with_jobs() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), - fuzz_for: Some(FuzzFor::Duration("10s".parse().expect("static valid duration string"))), + fuzz_for: Some("10s".parse().expect("static valid duration string")), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -206,6 +204,139 @@ fn test_execution_mode_replay_id_missing_centipede_binary_path_errors() { expect_true!(err_msg.contains("`--centipede-binary-path` needs to be specified")); } +#[gtest] +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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + 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 cmd = runner.build_run_command(&binary_path).expect("valid 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(); + expect_true!(envs.contains(&("FUZZTEST_REPLAY_FINDINGS".to_string(), Some("true".to_string())))); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--centipede-binary-path` needs to be specified")); +} + +#[gtest] +fn test_runner_build_run_command_with_replay_corpus() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + 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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + 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 cmd = runner.build_run_command(&binary_path).expect("valid 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(); + expect_true!( + envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("10s".to_string()))) + ); + expect_true!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +fn test_runner_build_run_command_with_replay_corpus_indefinitely() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + 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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + 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 cmd = runner.build_run_command(&binary_path).expect("valid 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(); + expect_true!( + envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string()))) + ); + expect_true!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--centipede-binary-path` needs to be specified")); +} + #[gtest] fn test_runner_list_command() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); @@ -221,3 +352,72 @@ fn test_runner_list_command() { let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); expect_eq!(args, &["__fuzztest_mod__", "--list"]); } + +#[gtest] +fn test_runner_build_run_command_with_list_crash_ids() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + 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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + 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 cmd = runner.build_run_command(&binary_path).expect("valid 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(); + expect_true!(envs.contains(&("FUZZTEST_LIST_CRASH_IDS".to_string(), Some("true".to_string())))); + expect_true!(envs.contains(&( + "FUZZTEST_LIST_CRASH_IDS_FILE".to_string(), + Some("/custom/path/to/crash_ids.txt".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +fn test_execution_mode_list_crash_ids_missing_corpus_db_errors() { + let fuzztest_options = FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + centipede_binary_path: Some("/custom/path/to/centipede".to_string()), + ..Default::default() + }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--corpus-db` needs to be specified")); +} + +#[gtest] +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()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--centipede-binary-path` needs to be specified")); +} diff --git a/rust/coverage/BUILD b/rust/coverage/BUILD index 38f65f7b3..3344a71c9 100644 --- a/rust/coverage/BUILD +++ b/rust/coverage/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -48,3 +48,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "coverage_clippy", + deps = [ + ":coverage", + ], +) diff --git a/rust/e2e_tests/replay_test.rs b/rust/e2e_tests/replay_test.rs index 767038afd..d14d7722c 100644 --- a/rust/e2e_tests/replay_test.rs +++ b/rust/e2e_tests/replay_test.rs @@ -33,11 +33,8 @@ fn get_target_binary_path(fixture: &EnvVars) -> PathBuf { #[gtest] fn replay_by_id_reproduces_panic(fixture: &EnvVars) { let test_name = "__fuzztest_mod__find_two_bugs_fuzz_test::find_two_bugs_fuzz_test"; - let normalized_test_name = test_name.replace("::", "."); let target_binary_path = get_target_binary_path(fixture); - let target_binary_str = target_binary_path.to_str().expect("valid path string"); - let binary_id = target_binary_str.strip_prefix('/').unwrap_or(target_binary_str); let db_dir = fixture.tmp_dir_path.join("replay_by_id_reproduces_panic").join("corpus_db"); fs::create_dir_all(&db_dir).expect("Failed to create db directory"); @@ -57,19 +54,19 @@ fn replay_by_id_reproduces_panic(fixture: &EnvVars) { .status() .expect("Failed to spawn binary"); - // 2. Retrieve the crash IDs from the corpus database using Centipede's --list_crash_ids flag. + // 2. Retrieve the crash IDs from the corpus database using FUZZTEST_LIST_CRASH_IDS. let crash_ids_file = fixture.tmp_dir_path.join("replay_by_id_reproduces_panic").join("crash_ids.txt"); - let list_args = [ - format!("--binary={}", target_binary_path.display()), - format!("--fuzztest_binary_identifier={}", binary_id), - format!("--test_name={}", normalized_test_name), - format!("--fuzztest_corpus_database={}", db_dir.display()), - "--list_crash_ids=1".to_string(), - format!("--list_crash_ids_file={}", crash_ids_file.display()), - ]; - let list_args_refs: Vec<&str> = list_args.iter().map(|s| s.as_str()).collect(); - test_utils::run_centipede_with_args_expect_termination(fixture, &list_args_refs); + let status = Command::new(&target_binary_path) + .arg(test_name) + .arg("--exact") + .env("FUZZTEST_LIST_CRASH_IDS", "true") + .env("FUZZTEST_LIST_CRASH_IDS_FILE", &crash_ids_file) + .env("FUZZTEST_CORPUS_DB", &db_dir) + .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) + .status() + .expect("Failed to execute target binary to list crash IDs"); + expect_true!(status.success()); let crash_ids_contents = fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); diff --git a/rust/engine/BUILD b/rust/engine/BUILD index 86036582d..42dc85354 100644 --- a/rust/engine/BUILD +++ b/rust/engine/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library") licenses(["notice"]) @@ -30,3 +30,10 @@ rust_library( "@com_google_fuzztest//centipede:engine_worker", ], ) + +rust_clippy( + name = "engine_clippy", + deps = [ + ":engine", + ], +) diff --git a/rust/engine/src/engine_ffi.rs b/rust/engine/src/engine_ffi.rs index 74badf48f..276b3e116 100644 --- a/rust/engine/src/engine_ffi.rs +++ b/rust/engine/src/engine_ffi.rs @@ -185,10 +185,7 @@ impl FuzzTestUint64sView { if self.data.is_null() { &[] } else { - ptr::slice_from_raw_parts( - self.data as *const u8, - self.size * core::mem::size_of::(), - ) + ptr::slice_from_raw_parts(self.data as *const u8, self.size * size_of::()) } } } diff --git a/rust/engine/src/lib.rs b/rust/engine/src/lib.rs index 58f9be3c8..f6e0bf838 100644 --- a/rust/engine/src/lib.rs +++ b/rust/engine/src/lib.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] + pub mod engine_ffi; use std::marker::PhantomData; diff --git a/rust/options/BUILD b/rust/options/BUILD index 2fc95f343..b56f9a655 100644 --- a/rust/options/BUILD +++ b/rust/options/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -42,3 +42,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "fuzztest_options_clippy", + deps = [ + ":fuzztest_options", + ], +) diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index 78145e539..93f9cc40c 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -12,11 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This module provides the core command-line flag and environment variable options -// structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`). +//! This module provides the core command-line flag and environment variable options +//! structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`). +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] + +use anyhow::Context; use clap::{Parser, ValueEnum}; -use humantime::Duration; +use std::fmt; +use std::fmt::Display; +use std::fmt::Formatter; +use std::str::FromStr; /// Time budget calculation type for replay corpus mode. #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -26,20 +33,6 @@ pub enum TimeBudgetType { Total, } -/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`. -/// -/// Matches `"inf"` or `"infinity"` to [`FuzzFor::Indefinitely`]. All other values -/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`). -fn parse_fuzz_for(s: &str) -> anyhow::Result { - let s_lower = s.trim().to_lowercase(); - if s_lower == "inf" || s_lower == "infinity" { - Ok(FuzzFor::Indefinitely) - } else { - let duration = s.parse()?; - Ok(FuzzFor::Duration(duration)) - } -} - /// Command-line and environment variable options parsed for the FuzzTest harness. #[derive(Parser, Debug, Clone, Default)] pub struct FuzzTestOptions { @@ -51,8 +44,8 @@ pub struct FuzzTestOptions { /// /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity` /// to fuzz indefinitely until a crash is found or it is stopped manually. - #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)] - pub fuzz_for: Option, + #[arg(env = "FUZZTEST_FUZZ_FOR", long)] + pub fuzz_for: Option, /// If true, subprocess logs are printed after every batch. Note that crash logs are always /// printed regardless of this flag's value. @@ -63,6 +56,24 @@ pub struct FuzzTestOptions { #[arg(env = "FUZZTEST_JOBS", long)] pub jobs: Option, + /// List all crash IDs stored in the corpus database. + #[arg( + env = "FUZZTEST_LIST_CRASH_IDS", + long, + requires = "corpus_db", + requires = "list_crash_ids_file" + )] + pub list_crash_ids: bool, + + /// The output file path where listed crash IDs will be written. + #[arg( + env = "FUZZTEST_LIST_CRASH_IDS_FILE", + long, + requires = "corpus_db", + requires = "list_crash_ids" + )] + pub list_crash_ids_file: Option, + /// The crash ID to be replayed from the corpus database. /// /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input @@ -71,12 +82,15 @@ pub struct FuzzTestOptions { pub replay_id: Option, /// Replay all crashing inputs from the corpus database. - #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long)] + #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long, requires = "corpus_db")] pub replay_findings: bool, /// Replay the corpus for a specified duration. - #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long)] - pub replay_corpus_for: Option, + /// + /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity` + /// to replay indefinitely until stopped manually. + #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long, requires = "corpus_db")] + pub replay_corpus_for: Option, /// Time budget calculation type for replay corpus mode. #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)] @@ -110,6 +124,9 @@ pub enum ExecutionMode { /// Replay corpus inputs for a specified duration. ReplayCorpus(ReplayCorpusOptions), + /// List crash IDs stored in the corpus database. + ListCrashIds(ListCrashIdsOptions), + /// List all discovered fuzz tests without running them. /// Currently only supported via `cargo-fuzztest`. ListFuzzTests, @@ -120,11 +137,21 @@ impl ExecutionMode { /// /// This decouples raw environment/CLI option parsing from execution mode validation. pub fn from_fuzztest_options(options: &FuzzTestOptions) -> ExecutionMode { + if options.list_crash_ids { + return ExecutionMode::ListCrashIds(ListCrashIdsOptions { + // TODO(the-shank): If a crash-ids file is not provided, write to stdout. + list_crash_ids_file: options + .list_crash_ids_file + .clone() + .expect("list_crash_ids_file should be set when list_crash_ids"), + }); + } + if let Some(replay_corpus_for) = options.replay_corpus_for { return ExecutionMode::ReplayCorpus(ReplayCorpusOptions { replay_corpus_for, time_budget_type: options.time_budget_type, - jobs: options.jobs.clone(), + jobs: options.jobs, }); } @@ -138,10 +165,7 @@ 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.clone(), - }); + return ExecutionMode::Fuzz(FuzzOptions { fuzz_for: *fuzz_for, jobs: options.jobs }); } ExecutionMode::SmokeTest @@ -151,21 +175,46 @@ impl ExecutionMode { /// Mode-specific options for continuous fuzzing. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FuzzOptions { - pub fuzz_for: FuzzFor, + pub fuzz_for: RunDuration, /// 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, } -/// The duration or limit for fuzzing. +/// The duration or limit for fuzzing or replaying corpus. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FuzzFor { - /// Fuzz indefinitely until it is manually stopped or a crash is found. +pub enum RunDuration { + /// Run indefinitely until manually stopped or a crash is found. Indefinitely, - /// Fuzz for a specific duration. - Duration(Duration), + /// Run for a specific fixed duration. + Fixed(humantime::Duration), +} + +impl FromStr for RunDuration { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let s_lower = s.trim().to_lowercase(); + if s_lower == "inf" || s_lower == "infinity" { + Ok(RunDuration::Indefinitely) + } else { + let duration: humantime::Duration = s + .parse() + .with_context(|| format!("while attempting to parse duration string '{s}'"))?; + Ok(RunDuration::Fixed(duration)) + } + } +} + +impl Display for RunDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + RunDuration::Indefinitely => write!(f, "inf"), + RunDuration::Fixed(duration) => write!(f, "{duration}"), + } + } } /// Mode-specific options for replaying a specific crashing input from the corpus database. @@ -177,19 +226,58 @@ pub struct ReplayCrashOptions { /// Mode-specific options for replaying corpus for a duration. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReplayCorpusOptions { - pub replay_corpus_for: Duration, + pub replay_corpus_for: RunDuration, pub time_budget_type: TimeBudgetType, /// 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, } +/// Mode-specific options for listing crash IDs from the database. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ListCrashIdsOptions { + pub list_crash_ids_file: String, +} + #[cfg(test)] mod tests { use super::*; use googletest::prelude::*; use std::ffi::OsString; + #[gtest] + fn test_duration_from_str_inf() { + let duration: RunDuration = "inf".parse().expect("failed to parse 'inf'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_infinity() { + let duration: RunDuration = "infinity".parse().expect("failed to parse 'infinity'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_fixed() { + let duration: RunDuration = "10s".parse().expect("failed to parse '10s'"); + let expected_fixed = humantime::Duration::from(std::time::Duration::from_secs(10)); + expect_that!(duration, eq(RunDuration::Fixed(expected_fixed))); + } + + #[gtest] + fn test_duration_from_str_invalid() { + let result: Result = "invalid_duration".parse(); + expect_true!(result.is_err()); + } + + #[gtest] + fn test_duration_display() { + expect_that!(RunDuration::Indefinitely.to_string(), eq("inf")); + let fixed = + RunDuration::Fixed(humantime::Duration::from(std::time::Duration::from_secs(10))); + expect_that!(fixed.to_string(), eq("10s")); + } + #[gtest] fn test_replay_id_requires_corpus_db() { // SAFETY: Testing environment parsing in single-threaded context. @@ -241,6 +329,7 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_that!(options.jobs, eq(Some(4))); + // Setting jobs alone should not enter fuzzing mode; it defaults to smoke test mode. expect_that!(ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::SmokeTest)); @@ -265,7 +354,7 @@ mod tests { expect_that!( ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: FuzzFor::Duration(expected_duration), + fuzz_for: RunDuration::Fixed(expected_duration), jobs: Some(4), })) ); @@ -283,12 +372,13 @@ mod tests { unsafe { std::env::set_var("FUZZTEST_JOBS", "4"); std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); } let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_that!(options.jobs, eq(Some(4))); - let expected_duration = "10s".parse().expect("valid duration"); + let expected_duration = "10s".parse().expect("valid duration string"); expect_that!( ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions { @@ -302,6 +392,287 @@ mod tests { unsafe { std::env::remove_var("FUZZTEST_JOBS"); std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + } + + #[gtest] + fn test_replay_findings_requires_corpus_db() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_FINDINGS", "true"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_FINDINGS"); + } + + let err = result.expect_err("parsing should fail when corpus_db is missing"); + expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); + } + + #[gtest] + 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"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_FINDINGS"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + 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"))); + } + + #[gtest] + fn test_replay_corpus_for_requires_corpus_db() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + } + + let err = result.expect_err("parsing should fail when corpus_db is missing"); + expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); + } + + #[gtest] + fn test_replay_corpus_for_with_corpus_db_succeeds() { + // 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"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result + .expect("parsing should succeed when both replay_corpus_for and corpus_db are present"); + expect_that!( + 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.time_budget_type, eq(TimeBudgetType::PerTest)); + } + + #[gtest] + fn test_replay_corpus_for_inf_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "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"))); + } + + #[gtest] + fn test_replay_corpus_for_infinity_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "infinity"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "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"))); + } + + #[gtest] + fn test_replay_corpus_for_with_total_time_budget() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); + std::env::set_var("FUZZTEST_TIME_BUDGET_TYPE", "total"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_TIME_BUDGET_TYPE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect("parsing should succeed with total time budget type"); + expect_that!( + 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.time_budget_type, eq(TimeBudgetType::Total)); + } + + #[gtest] + fn test_replay_corpus_for_inf_with_total_time_budget() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_TIME_BUDGET_TYPE", "total"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_TIME_BUDGET_TYPE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); } + + 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.time_budget_type, eq(TimeBudgetType::Total)); + } + + #[gtest] + fn test_list_crash_ids_requires_corpus_db() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + } + + let err = result.expect_err("parsing should fail when corpus_db is missing"); + expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); + } + + #[gtest] + fn test_list_crash_ids_requires_list_crash_ids_file() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let err = result.expect_err("parsing should fail when list_crash_ids_file is missing"); + expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); + } + + #[gtest] + fn test_list_crash_ids_file_requires_list_crash_ids() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let err = result.expect_err("parsing should fail when list_crash_ids is missing"); + expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); + } + + #[gtest] + fn test_list_crash_ids_with_corpus_db_and_file_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "parsing should succeed when list_crash_ids, list_crash_ids_file, and corpus_db are present", + ); + 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"))); + + let mode = ExecutionMode::from_fuzztest_options(&options); + expect_that!( + mode, + eq(&ExecutionMode::ListCrashIds(ListCrashIdsOptions { + list_crash_ids_file: "/tmp/crashes.txt".to_string() + })) + ); } } diff --git a/rust/src/domains.rs b/rust/src/domains.rs index 0447752a4..a873c5990 100644 --- a/rust/src/domains.rs +++ b/rust/src/domains.rs @@ -17,6 +17,8 @@ pub mod containers; pub mod range; pub mod tuple_of; pub mod utility; +use ::serde::de::DeserializeOwned; +use ::serde::Serialize; use anyhow; use anyhow::Context; @@ -118,7 +120,7 @@ pub trait Domain { /// the CorpusValue could the owned data structured that the `&str` points to (eg: String). /// The CorpusValue type should implement `serde::Serialize`, `serde::de::DeserializeOwned` and /// `Clone`. - type CorpusValue: ::serde::Serialize + ::serde::de::DeserializeOwned + Clone; + type CorpusValue: Serialize + DeserializeOwned + Clone; /// Initializes a new value drawn from the domain. fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result; diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs index 3ad8bdbad..e2167bf55 100644 --- a/rust/src/domains/arbitrary.rs +++ b/rust/src/domains/arbitrary.rs @@ -16,6 +16,9 @@ use super::utility::choose_value; use super::utility::mutate_integer; use super::utility::shrink_towards; use super::Domain; +use std::char; +use std::fmt; +use std::marker::PhantomData; use anyhow; use rand::RngExt; @@ -39,19 +42,18 @@ use rand::RngExt; /// let sample = arbitrary_i32.init(&mut rng); /// assert!(sample.is_ok()); /// ``` - pub struct Arbitrary { - _phantom: std::marker::PhantomData, + _phantom: PhantomData, } impl Clone for Arbitrary { fn clone(&self) -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } -impl std::fmt::Debug for Arbitrary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Arbitrary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Arbitrary").field("_phantom", &self._phantom).finish() } } @@ -59,14 +61,14 @@ impl std::fmt::Debug for Arbitrary { // We cannot just use `#[derive(Default)]` because `T` might not be `Default`. impl Default for Arbitrary { fn default() -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } impl Arbitrary { /// Creates a new `Arbitrary` domain for the given type `T`. pub fn new() -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } @@ -254,7 +256,7 @@ fn map_int_to_char(u: u32) -> char { NUM_VALID_CODEPOINTS ); let val = if u >= SURROGATE_START { u + (SURROGATE_END - SURROGATE_START + 1) } else { u }; - std::char::from_u32(val).unwrap() + char::from_u32(val).unwrap() } impl Domain for Arbitrary { @@ -745,19 +747,13 @@ mod tests { assert_eq!(map_int_to_char(0), '\u{0000}'); let before_surrogate = SURROGATE_START - 1; - assert_eq!( - map_char_to_int(std::char::from_u32(before_surrogate).unwrap()), - before_surrogate - ); - assert_eq!( - map_int_to_char(before_surrogate), - std::char::from_u32(before_surrogate).unwrap() - ); + assert_eq!(map_char_to_int(char::from_u32(before_surrogate).unwrap()), before_surrogate); + assert_eq!(map_int_to_char(before_surrogate), char::from_u32(before_surrogate).unwrap()); let after_surrogate = SURROGATE_END + 1; - let mapped_after_surrogate = map_char_to_int(std::char::from_u32(after_surrogate).unwrap()); + let mapped_after_surrogate = map_char_to_int(char::from_u32(after_surrogate).unwrap()); assert_eq!(mapped_after_surrogate, SURROGATE_START); - assert_eq!(map_int_to_char(SURROGATE_START), std::char::from_u32(after_surrogate).unwrap()); + assert_eq!(map_int_to_char(SURROGATE_START), char::from_u32(after_surrogate).unwrap()); assert_eq!(map_char_to_int('\u{10FFFF}'), NUM_VALID_CODEPOINTS - 1); assert_eq!(map_int_to_char(NUM_VALID_CODEPOINTS - 1), '\u{10FFFF}'); @@ -776,7 +772,7 @@ mod tests { while value != '\0' && iterations < MAX_ITERATIONS { domain.mutate(&mut value, &mut rng, true).unwrap(); // Ensure that the value is always a valid char after mutation. - assert!(std::char::from_u32(value as u32).is_some()); + assert!(char::from_u32(value as u32).is_some()); iterations += 1; } assert_eq!( diff --git a/rust/src/domains/containers.rs b/rust/src/domains/containers.rs index 3c34565c9..a15e28ca2 100644 --- a/rust/src/domains/containers.rs +++ b/rust/src/domains/containers.rs @@ -1,4 +1,5 @@ use rand::RngExt; +use std::fmt; use super::Domain; @@ -99,8 +100,8 @@ impl Clone for VecOf { } } -impl std::fmt::Debug for VecOf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for VecOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("VecOf") .field("inner", &self.inner) .field("min_len", &self.min_len) @@ -201,7 +202,7 @@ impl ContainerDomain for VecOf { fn with_min_len(self, min_len: usize) -> Self { assert!( - self.max_len.map_or(true, |max| min_len <= max), + self.max_len.is_none_or(|max| min_len <= max), "Minimum length {} cannot be greater than the maximum length {}", min_len, self.max_len.unwrap() diff --git a/rust/src/domains/utility.rs b/rust/src/domains/utility.rs index 2ce15fa32..e14bcef02 100644 --- a/rust/src/domains/utility.rs +++ b/rust/src/domains/utility.rs @@ -18,6 +18,7 @@ use num_traits::PrimInt; use rand::distr::uniform::SampleUniform; use rand::distr::{Distribution, StandardUniform}; use rand::RngExt; +use std::fmt::Display; /// Shrinks a `val` towards a `target` value. /// @@ -32,7 +33,7 @@ use rand::RngExt; /// * `target`: The value to shrink towards. pub fn shrink_towards(rng: &mut R, val: T, target: T) -> T where - T: SampleUniform + PartialOrd + Copy + std::fmt::Display, + T: SampleUniform + PartialOrd + Copy + Display, { match val.partial_cmp(&target) { Some(Ordering::Equal) => val, @@ -81,7 +82,7 @@ pub fn mutate_integer( max_value: Option, ) -> T where - T: PrimInt + SampleUniform + std::fmt::Display, + T: PrimInt + SampleUniform + Display, { assert!(range > T::zero(), "mutate_integer: range value cannot be <= 0: {range}"); @@ -106,7 +107,7 @@ where } 1 => { // 1/3 chance: Flip a random bit - let num_bits = std::mem::size_of::() * 8; + let num_bits = size_of::() * 8; let bit_index = rng.random_range(0..num_bits); let mask = T::one() << bit_index; let result = val ^ mask; @@ -194,7 +195,7 @@ impl SpecialValues for char { /// /// # Type Parameters /// * `T`: The type of the value to choose. Must implement `SpecialValues` and -/// `StandardUniform` must be able to generate values of type `T`. +/// `StandardUniform` must be able to generate values of type `T`. pub fn choose_value(rng: &mut R) -> T where T: SpecialValues + 'static, @@ -217,7 +218,7 @@ pub fn mutate_float( _range: Option<(T, T)>, // TODO: Implement range support for floats. ) -> anyhow::Result<()> where - T: num_traits::Float + SampleUniform + std::fmt::Display + Copy + SpecialValues + 'static, + T: num_traits::Float + SampleUniform + Display + Copy + SpecialValues + 'static, StandardUniform: Distribution, { if only_shrink { @@ -259,6 +260,7 @@ mod tests { rngs::{SmallRng, SysRng}, SeedableRng, }; + use std::fmt::Debug; fn get_rng() -> SmallRng { SmallRng::try_from_rng(&mut SysRng).unwrap() @@ -266,7 +268,7 @@ mod tests { fn check_shrink_towards(smaller: T, larger: T) where - T: SampleUniform + PartialOrd + Copy + std::fmt::Display + std::fmt::Debug + PartialEq, + T: SampleUniform + PartialOrd + Copy + Display + Debug + PartialEq, { let mut rng = get_rng(); @@ -317,7 +319,7 @@ mod tests { fn check_mutate_integer() where - T: PrimInt + SampleUniform + std::fmt::Display + std::fmt::Debug + SpecialValues + 'static, + T: PrimInt + SampleUniform + Display + Debug + SpecialValues + 'static, StandardUniform: Distribution, { let mut rng = get_rng(); @@ -369,13 +371,7 @@ mod tests { fn check_mutate_float() where - T: num_traits::Float - + SampleUniform - + std::fmt::Display - + std::fmt::Debug - + Copy - + SpecialValues - + 'static, + T: num_traits::Float + SampleUniform + Display + Debug + Copy + SpecialValues + 'static, StandardUniform: Distribution, { let mut rng = get_rng(); diff --git a/rust/src/internal.rs b/rust/src/internal.rs index c83c1575b..213e66aa3 100644 --- a/rust/src/internal.rs +++ b/rust/src/internal.rs @@ -33,7 +33,7 @@ pub trait FuzzTest { /// (will attempt to downcast to actual user values). /// /// Returns `true` if the property function holds, `false` if it crashes. - fn execute<'a>(&self, args: &'a GenericCorpusValue) -> bool; + fn execute(&self, args: &GenericCorpusValue) -> bool; fn print_finding_report(&self); fn domains(&self) -> &dyn GenericDomain; } @@ -57,6 +57,7 @@ pub struct FuzzTestRegistration { inventory::collect!(FuzzTestRegistration); +#[allow(clippy::type_complexity)] pub static FUZZ_TEST_NAME_TO_FACTORY: LazyLock BoxedFuzzTest>> = LazyLock::new(|| { inventory::iter diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6a8d7a11b..bc663ef5b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] #![feature(cfg_sanitize)] mod crash_handler; diff --git a/rust/src/options.rs b/rust/src/options.rs index 2c8cbcf0b..41abaf5a4 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -16,15 +16,17 @@ use crate::internal::FuzzTestRegistration; use ::engine::engine_ffi; use anyhow::Context; use clap::Parser; +use std::env; use std::ffi::CString; use std::ffi::OsString; +use std::iter; use std::path::Path; use std::sync::OnceLock; use tempfile::{NamedTempFile, TempDir}; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, - TimeBudgetType, + ExecutionMode, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, + RunDuration, TimeBudgetType, }; /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`. @@ -34,7 +36,7 @@ pub fn get_fuzztest_options() -> &'static FuzzTestOptions { // from environment variables (like `FUZZTEST_FUZZ_FOR` etc.). We (currently) do not envisage // support for passing flags on cli as the Rust's libtest harness does not support custom // flags. - OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(std::iter::empty::())) + OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(iter::empty::())) } trait ExecutionModeExt { @@ -59,7 +61,8 @@ impl ExecutionModeExt for ExecutionMode { ExecutionMode::SmokeTest => Ok(ExecutionAction::SmokeTest), ExecutionMode::Fuzz(_) | ExecutionMode::ReplayCrash(_) - | ExecutionMode::ReplayCorpus(_) => { + | ExecutionMode::ReplayCorpus(_) + | ExecutionMode::ListCrashIds(_) => { let centipede_args = CentipedeArgs::from_execution_mode(self, options, current_test_name, None)? .context( @@ -154,7 +157,7 @@ impl CentipedeArgs { // ============================================================================== // 1. Common Base Arguments (Required across all Centipede executions) // ============================================================================== - let argv0 = std::env::args().next().context("while attempting to get argv[0]")?; + let argv0 = env::args().next().context("while attempting to get argv[0]")?; add_arg(format!("--binary={argv0} {current_test_name} --exact --nocapture"))?; let normalized_test_name = current_test_name.replace("::", "."); @@ -211,10 +214,10 @@ impl CentipedeArgs { match mode_opts { ExecutionMode::Fuzz(fuzz_opts) => { match &fuzz_opts.fuzz_for { - FuzzFor::Indefinitely => { + RunDuration::Indefinitely => { // not specifying `--stop_after` means to run indefinitely. } - FuzzFor::Duration(duration) => { + RunDuration::Fixed(duration) => { let duration_secs = duration.as_secs_f64(); if opt_corpusdb.is_some() { add_arg(format!("--fuzztest_time_limit_per_test={duration_secs}s"))?; @@ -239,26 +242,39 @@ impl CentipedeArgs { add_arg(format!("--list_crash_ids_file={}", path.display()))?; } ExecutionMode::ReplayCorpus(replay_corpus_opts) => { - let time_limit = match replay_corpus_opts.time_budget_type { - TimeBudgetType::PerTest => replay_corpus_opts.replay_corpus_for, - TimeBudgetType::Total => { - let num_tests = inventory::iter::().count(); - if num_tests == 0 { - replay_corpus_opts.replay_corpus_for - } else { - (*replay_corpus_opts.replay_corpus_for.as_ref() / (num_tests as u32)) - .into() - } - } - }; add_arg("--fuzztest_only_replay=true".to_string())?; add_arg("--fuzztest_replay_coverage_inputs=true".to_string())?; add_arg("--load_shards_only=true".to_string())?; - add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?; + match replay_corpus_opts.replay_corpus_for { + RunDuration::Indefinitely => { + // Not specifying `--fuzztest_time_limit_per_test` means to run indefinitely + } + RunDuration::Fixed(duration) => { + let time_limit: humantime::Duration = match replay_corpus_opts + .time_budget_type + { + TimeBudgetType::PerTest => duration, + TimeBudgetType::Total => { + let num_tests = inventory::iter::().count(); + if num_tests == 0 { + duration + } else { + (*duration.as_ref() / (num_tests as u32)).into() + } + } + }; + let time_limit_secs = time_limit.as_secs_f64(); + add_arg(format!("--fuzztest_time_limit_per_test={time_limit_secs}s"))?; + } + } if let Some(jobs) = &replay_corpus_opts.jobs { add_arg(format!("--j={jobs}"))?; } } + ExecutionMode::ListCrashIds(list_opts) => { + add_arg("--list_crash_ids=true".to_string())?; + add_arg(format!("--list_crash_ids_file={}", list_opts.list_crash_ids_file))?; + } ExecutionMode::SmokeTest => unreachable!(), ExecutionMode::ListFuzzTests => unreachable!(), } @@ -390,12 +406,12 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); - expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely))); + expect_that!(options.fuzz_for, eq(Some(RunDuration::Indefinitely))); let mode = ExecutionMode::from_fuzztest_options(&options); let ExecutionMode::Fuzz(fuzz_opts) = mode else { panic!("Expected ExecutionMode::Fuzz"); }; - expect_that!(fuzz_opts.fuzz_for, eq(FuzzFor::Indefinitely)); + expect_that!(fuzz_opts.fuzz_for, eq(RunDuration::Indefinitely)); // SAFETY: Cleaning up environment variables. unsafe { @@ -406,7 +422,7 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone_indefinite() { let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -430,11 +446,8 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone() { - let expected_duration = "10s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let expected_duration: RunDuration = "10s".parse().unwrap(); + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -464,10 +477,7 @@ mod tests { #[gtest] fn test_centipede_args_binary_identifier() { let expected_duration = "1s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -487,10 +497,8 @@ mod tests { #[gtest] fn test_centipede_args_jobs() { let expected_duration = "1s".parse().expect("failed to parse duration"); - let options_no_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options_no_jobs = + FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action_no_jobs = determine_execution_action_internal(&options_no_jobs, "my_mod::my_test"); let ExecutionAction::Standalone(args_no_jobs) = action_no_jobs else { @@ -501,7 +509,7 @@ mod tests { assert!(!args_str_no_jobs.iter().any(|s| s.starts_with("--j="))); let options_with_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), + fuzz_for: Some(expected_duration), jobs: Some(4), ..Default::default() }; @@ -518,8 +526,7 @@ mod tests { #[gtest] fn test_default_env_diff_set() { let duration = "1s".parse().unwrap(); - let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration(duration)), ..Default::default() }; + let options = FuzzTestOptions { fuzz_for: Some(duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { panic!("Expected Standalone action"); @@ -633,6 +640,31 @@ mod tests { assert!(args_str.contains(&"--fuzztest_time_limit_per_test=10s")); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::PerTest, + ..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().expect("invalid utf8")).collect(); + + assert!(args_str + .iter() + .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact"))); + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(args_str.contains(&"--fuzztest_replay_coverage_inputs=true")); + assert!(args_str.contains(&"--load_shards_only=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_determine_execution_action_replay_corpus_total_budget() { let expected_duration = "10s".parse().expect("failed to parse duration"); @@ -651,16 +683,39 @@ mod tests { args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); let num_tests = inventory::iter::().count(); + let RunDuration::Fixed(fixed_duration) = expected_duration else { + panic!("expected Fixed duration"); + }; let expected_limit = if num_tests == 0 { - expected_duration + fixed_duration } else { - (*expected_duration.as_ref() / (num_tests as u32)).into() + (*fixed_duration.as_ref() / (num_tests as u32)).into() }; let expected_limit_str = format!("--fuzztest_time_limit_per_test={}", expected_limit); assert!(args_str.contains(&expected_limit_str.as_str())); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite_total_budget() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + ..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().expect("invalid utf8")).collect(); + + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_get_corpusdb_and_workdir_default_creates_temp_workdir() -> Result<()> { let options = FuzzTestOptions::default(); @@ -745,11 +800,8 @@ mod tests { // When corpus_db is not provided, fuzzing for a fixed duration should pass --stop_after // to Centipede so it stops fuzzing after the specified duration. let duration = "1s".parse().expect("fixed test string should parse as duration"); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), - corpus_db: None, - ..Default::default() - }; + let options = + FuzzTestOptions { fuzz_for: Some(duration), corpus_db: None, ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -773,7 +825,7 @@ mod tests { // a corpus database. let duration = "1s".parse().expect("fixed test string should parse as duration"); let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), + fuzz_for: Some(duration), corpus_db: Some("/tmp/corpus_db".to_string()), ..Default::default() }; @@ -793,4 +845,28 @@ mod tests { expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db")); expect_false!(args_str.iter().any(|s| s.starts_with("--stop_after="))); } + + #[gtest] + fn test_determine_execution_action_list_crash_ids() { + 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()), + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action for ListCrashIds"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + expect_true!(args_str.contains(&"--list_crash_ids=true")); + expect_true!(args_str.contains(&"--list_crash_ids_file=/tmp/custom_crashes.txt")); + expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db")); + expect_true!(args_str.iter().any(|s| s.starts_with("--workdir="))); + expect_true!(args_str.contains(&"--test_name=my_mod.my_test")); + } } diff --git a/rust/src/worker.rs b/rust/src/worker.rs index 20ac83ff8..8eae57fa8 100644 --- a/rust/src/worker.rs +++ b/rust/src/worker.rs @@ -20,10 +20,16 @@ use ::engine::{ BytesSink, CoverageDomainRegistry, DiagnosticSink, ExecuteContext, FeedbackSink, InputSink, }; use spin::Mutex; +use std::env; +use std::ffi::c_int; use std::ffi::CString; +use std::fs; use std::path::Path; +use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::LazyLock; +use std::time::Duration; +use std::time::Instant; /// The DiagnosticSink provided by the engine while creating the adapter. /// @@ -59,7 +65,9 @@ pub(crate) fn clear_diagnostic_sink() { /// This function blocks till it can get the lock on the global DiagnosticSink and is not /// signal-safe. pub(crate) fn emit_error(message: &str) { - DIAGNOSTIC_SINK.lock().as_ref().map(|sink| sink.emit_error(message)); + if let Some(sink) = DIAGNOSTIC_SINK.lock().as_ref() { + sink.emit_error(message) + } } /// Emits a finding into the DiagnosticSink if the DiagnosticSink is set. @@ -99,7 +107,7 @@ pub(crate) fn try_emit_finding(description: &str, signature: &str) -> bool { return false; }; sink.emit_finding(token, description, signature); - return true; + true } // We double-box the input because `GenericCorpusValue` is a fat pointer (`Box`), @@ -252,7 +260,7 @@ impl RustFuzzTestAdapterManager { pub fn get_binary_id(&self, sink: &mut BytesSink) { static ARGV0: LazyLock = LazyLock::new(|| { CString::new( - Path::new(&std::env::args().nth(0).unwrap()) + Path::new(&env::args().next().unwrap()) .file_name() .and_then(|f| f.to_str()) .unwrap_or(""), @@ -428,7 +436,7 @@ pub unsafe extern "C" fn get_random_seed_input_callback( pub unsafe extern "C" fn mutate_callback( ctx: *mut engine_ffi::FuzzTestAdapterCtx, origin: engine_ffi::FuzzTestInputHandle, - shrink: std::ffi::c_int, + shrink: c_int, sink: *const engine_ffi::FuzzTestInputSink, ) { // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter` @@ -620,10 +628,10 @@ pub unsafe extern "C" fn free_ctx_callback(ctx: *mut engine_ffi::FuzzTestAdapter } pub fn run_smoke_test(fuzztest: &dyn FuzzTest) { - let start_time = std::time::Instant::now(); + let start_time = Instant::now(); // TODO(the-shank): these should be configurable externally. - let smoke_test_duration = std::time::Duration::from_secs(1); + let smoke_test_duration = Duration::from_secs(1); let only_shrink = false; // TODO(the-shank): the rng seed should be configurable @@ -681,7 +689,7 @@ pub fn process(manager: RustFuzzTestAdapterManager) { return; } WorkerStatus::Failure => { - std::process::exit(1); + process::exit(1); } } } @@ -701,7 +709,7 @@ pub fn process(manager: RustFuzzTestAdapterManager) { } // Now read the file and replay each crash - if let Ok(contents) = std::fs::read_to_string(list_file.path()) { + if let Ok(contents) = fs::read_to_string(list_file.path()) { let options = options::get_fuzztest_options(); for crash_id in contents.lines() { let crash_id = crash_id.trim();