From 412c2c6497e5af5fc2bb3b60471a8108f4cca095 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Wed, 19 Aug 2026 10:18:07 -0700 Subject: [PATCH] fuzztest-rust | resolve relative corpus_db paths in test environments - Change corpus_db field in FuzzTestOptions from String to PathBuf. - Add parse_corpus_db_path to resolve relative corpus_db paths relative to TEST_SRCDIR when running inside Bazel test sandboxes. - Disallow empty corpus_db paths during CLI and environment parsing. PiperOrigin-RevId: 967278954 --- rust/cargo_fuzztest/src/lib.rs | 28 +++++++-------- rust/cargo_fuzztest/tests/runner_test.rs | 18 +++++----- rust/options/src/lib.rs | 44 +++++++++++++++++------- rust/src/options.rs | 27 +++++++++------ 4 files changed, 72 insertions(+), 45 deletions(-) diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index e2af0905d..353e26fd6 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -447,7 +447,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 +475,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 +490,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 +531,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 +542,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 +557,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 +600,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!( @@ -628,7 +628,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!( @@ -700,7 +700,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 +716,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 +765,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 +797,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 +813,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 +850,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()), diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index 14bd430b0..a7c81c9c5 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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/options/src/lib.rs b/rust/options/src/lib.rs index 93f9cc40c..5abe89c4d 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,8 @@ 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, } /// Strongly-typed domain execution mode for test runs. @@ -316,7 +336,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] @@ -419,8 +439,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 +454,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 +498,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 +522,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 +545,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 +571,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 +595,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 +685,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..b5c61d205 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -340,11 +340,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 +557,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 +580,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 +738,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 +752,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 +799,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 +833,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 +858,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");