Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion rust/cargo_fuzztest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl CargoFuzzTestOptions {
.unwrap_or(RunDuration::Indefinitely),
jobs: self.fuzztest_options.jobs,
continue_after_crash: self.fuzztest_options.continue_after_crash,
execution_id: self.fuzztest_options.execution_id.clone(),
})
} else {
mode
Expand Down Expand Up @@ -249,14 +250,18 @@ impl FuzztestRunner {
}

ExecutionMode::Fuzz(fuzz_options) => {
let FuzzOptions { fuzz_for, jobs, continue_after_crash } = fuzz_options;
let FuzzOptions { fuzz_for, jobs, continue_after_crash, execution_id } =
fuzz_options;
cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string());
if let Some(jobs) = jobs {
cmd.env("FUZZTEST_JOBS", jobs.to_string());
}
if continue_after_crash {
cmd.env("FUZZTEST_CONTINUE_AFTER_CRASH", "true");
}
if let Some(execution_id) = execution_id {
cmd.env("FUZZTEST_EXECUTION_ID", execution_id);
}
}

ExecutionMode::ReplayCrash(replay_options) => {
Expand All @@ -280,6 +285,9 @@ impl FuzztestRunner {
if replay_corpus_options.continue_after_crash {
cmd.env("FUZZTEST_CONTINUE_AFTER_CRASH", "true");
}
if let Some(execution_id) = replay_corpus_options.execution_id {
cmd.env("FUZZTEST_EXECUTION_ID", execution_id);
}
}

ExecutionMode::ListCrashIds(list_crash_ids_options) => {
Expand Down Expand Up @@ -617,6 +625,7 @@ mod tests {
time_budget_type: TimeBudgetType::PerTest,
jobs: None,
continue_after_crash: false,
execution_id: None,
})
);
}
Expand Down Expand Up @@ -646,6 +655,7 @@ mod tests {
time_budget_type: TimeBudgetType::PerTest,
jobs: None,
continue_after_crash: false,
execution_id: None,
})
);
}
Expand All @@ -672,6 +682,7 @@ mod tests {
time_budget_type: TimeBudgetType::PerTest,
jobs: None,
continue_after_crash: false,
execution_id: None,
})
);
}
Expand Down Expand Up @@ -702,6 +713,7 @@ mod tests {
time_budget_type: TimeBudgetType::Total,
jobs: None,
continue_after_crash: false,
execution_id: None,
})
);
}
Expand Down Expand Up @@ -912,6 +924,7 @@ mod tests {
fuzz_for: RunDuration::Fixed(expected_duration),
jobs: None,
continue_after_crash: true,
execution_id: None,
})
);
}
Expand Down Expand Up @@ -972,4 +985,64 @@ mod tests {
);
assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("10s".to_string()))));
}

#[gtest]
fn test_build_run_command_fuzz_with_execution_id() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
fuzz_for: Some("5s".parse().unwrap()),
corpus_db: Some("/tmp/corpus_db".into()),
execution_id: Some("exec_123".to_string()),
..Default::default()
},
centipede_binary_path: Some("/custom/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| {
(k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
})
.collect();

assert!(envs.contains(&("FUZZTEST_EXECUTION_ID".to_string(), Some("exec_123".to_string()))));
assert!(
envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string())))
);
assert!(envs.contains(&("FUZZTEST_FUZZ_FOR".to_string(), Some("5s".to_string()))));
}

#[gtest]
fn test_build_run_command_replay_corpus_with_execution_id() {
let options = CargoFuzzTestOptions {
fuzztest_options: FuzzTestOptions {
replay_corpus_for: Some("10s".parse().expect("valid duration")),
corpus_db: Some("/tmp/corpus_db".into()),
execution_id: Some("exec_456".to_string()),
..Default::default()
},
centipede_binary_path: Some("/custom/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| {
(k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
})
.collect();

assert!(envs.contains(&("FUZZTEST_EXECUTION_ID".to_string(), Some("exec_456".to_string()))));
assert!(
envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string())))
);
assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("10s".to_string()))));
}
}
62 changes: 62 additions & 0 deletions rust/cargo_fuzztest/tests/runner_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,65 @@ fn test_execution_mode_list_crash_ids_missing_centipede_binary_path_errors() {
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_execution_id_fuzz() {
let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
let fuzztest_options = FuzzTestOptions {
fuzz_for: Some("5s".parse().unwrap()),
corpus_db: Some("/custom/path/to/corpus_db".into()),
execution_id: Some("exec_workflow_123".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("sample-host-triple".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
.collect();
expect_true!(envs
.contains(&("FUZZTEST_EXECUTION_ID".to_string(), Some("exec_workflow_123".to_string()))));
expect_true!(envs.contains(&(
"FUZZTEST_CORPUS_DB".to_string(),
Some("/custom/path/to/corpus_db".to_string())
)));
expect_true!(envs.contains(&("FUZZTEST_FUZZ_FOR".to_string(), Some("5s".to_string()))));
}

#[gtest]
fn test_runner_build_run_command_with_execution_id_replay_corpus() {
let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
let fuzztest_options = FuzzTestOptions {
replay_corpus_for: Some("10s".parse().unwrap()),
corpus_db: Some("/custom/path/to/corpus_db".into()),
execution_id: Some("exec_workflow_456".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("sample-host-triple".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
.collect();
expect_true!(envs
.contains(&("FUZZTEST_EXECUTION_ID".to_string(), Some("exec_workflow_456".to_string()))));
expect_true!(envs.contains(&(
"FUZZTEST_CORPUS_DB".to_string(),
Some("/custom/path/to/corpus_db".to_string())
)));
expect_true!(
envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("10s".to_string())))
);
}
109 changes: 109 additions & 0 deletions rust/options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ pub struct FuzzTestOptions {
/// crashing inputs regardless of this flag.
#[arg(env = "FUZZTEST_CONTINUE_AFTER_CRASH", long)]
pub continue_after_crash: bool,

/// The execution identifier for the fuzz test run.
///
/// When specified with `corpus_db`, allows resuming interrupted fuzzing or corpus replay
/// sessions, or skipping tests that already finished with the same execution ID.
#[arg(env = "FUZZTEST_EXECUTION_ID", long, requires = "corpus_db")]
pub execution_id: Option<String>,
}

/// Strongly-typed domain execution mode for test runs.
Expand Down Expand Up @@ -186,6 +193,7 @@ impl ExecutionMode {
time_budget_type: options.time_budget_type,
jobs: options.jobs,
continue_after_crash: options.continue_after_crash,
execution_id: options.execution_id.clone(),
});
}

Expand All @@ -203,6 +211,7 @@ impl ExecutionMode {
fuzz_for: *fuzz_for,
jobs: options.jobs,
continue_after_crash: options.continue_after_crash,
execution_id: options.execution_id.clone(),
});
}

Expand All @@ -220,6 +229,8 @@ pub struct FuzzOptions {
pub jobs: Option<usize>,

pub continue_after_crash: bool,

pub execution_id: Option<String>,
}

/// The duration or limit for fuzzing or replaying corpus.
Expand Down Expand Up @@ -272,6 +283,7 @@ pub struct ReplayCorpusOptions {
/// will use its own default value.
pub jobs: Option<usize>,
pub continue_after_crash: bool,
pub execution_id: Option<String>,
}

/// Mode-specific options for listing crash IDs from the database.
Expand Down Expand Up @@ -398,6 +410,7 @@ mod tests {
fuzz_for: RunDuration::Fixed(expected_duration),
jobs: Some(4),
continue_after_crash: false,
execution_id: None,
}))
);

Expand Down Expand Up @@ -428,6 +441,7 @@ mod tests {
time_budget_type: TimeBudgetType::PerTest,
jobs: Some(4),
continue_after_crash: false,
execution_id: None,
}))
);

Expand Down Expand Up @@ -456,6 +470,7 @@ mod tests {
fuzz_for: RunDuration::Fixed(expected_duration),
jobs: None,
continue_after_crash: true,
execution_id: None,
}))
);

Expand Down Expand Up @@ -485,6 +500,7 @@ mod tests {
time_budget_type: TimeBudgetType::PerTest,
jobs: None,
continue_after_crash: true,
execution_id: None,
}))
);

Expand Down Expand Up @@ -775,4 +791,97 @@ mod tests {
}))
);
}

#[gtest]
fn test_execution_id_requires_corpus_db() {
// SAFETY: Testing environment parsing in single-threaded context.
unsafe {
std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123");
std::env::remove_var("FUZZTEST_CORPUS_DB");
}

let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());

// SAFETY: Cleaning up environment variables.
unsafe {
std::env::remove_var("FUZZTEST_EXECUTION_ID");
}

let err =
result.expect_err("parsing should fail when corpus_db is missing for execution_id");
expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument));
}

#[gtest]
fn test_execution_id_with_corpus_db_and_fuzz_for_succeeds() {
// SAFETY: Testing environment parsing in single-threaded context.
unsafe {
std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123");
std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
std::env::set_var("FUZZTEST_FUZZ_FOR", "5s");
}

let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());

// SAFETY: Cleaning up environment variables.
unsafe {
std::env::remove_var("FUZZTEST_EXECUTION_ID");
std::env::remove_var("FUZZTEST_CORPUS_DB");
std::env::remove_var("FUZZTEST_FUZZ_FOR");
}

let options = result.expect(
"parsing should succeed when execution_id, corpus_db, and fuzz_for are present",
);
expect_that!(options.execution_id.as_deref(), eq(Some("exec_123")));
expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));

let expected_duration = "5s".parse().expect("valid duration");
expect_that!(
ExecutionMode::from_fuzztest_options(&options),
eq(&ExecutionMode::Fuzz(FuzzOptions {
fuzz_for: RunDuration::Fixed(expected_duration),
jobs: None,
continue_after_crash: false,
execution_id: Some("exec_123".to_string()),
}))
);
}

#[gtest]
fn test_execution_id_with_corpus_db_and_replay_corpus_succeeds() {
// SAFETY: Testing environment parsing in single-threaded context.
unsafe {
std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123");
std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s");
}

let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());

// SAFETY: Cleaning up environment variables.
unsafe {
std::env::remove_var("FUZZTEST_EXECUTION_ID");
std::env::remove_var("FUZZTEST_CORPUS_DB");
std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR");
}

let options = result.expect(
"parsing should succeed when execution_id, corpus_db, and replay_corpus_for are present",
);
expect_that!(options.execution_id.as_deref(), eq(Some("exec_123")));
expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));

let expected_duration = "10s".parse().expect("valid duration string");
expect_that!(
ExecutionMode::from_fuzztest_options(&options),
eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
replay_corpus_for: expected_duration,
time_budget_type: TimeBudgetType::PerTest,
jobs: None,
continue_after_crash: false,
execution_id: Some("exec_123".to_string()),
}))
);
}
}
Loading
Loading