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
113 changes: 110 additions & 3 deletions rust/cargo_fuzztest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
use anyhow::{Context, Result};
use clap::Parser;
pub use fuzztest_options::{
ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions,
TimeBudgetType,
ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions,
ReplayCrashOptions, TimeBudgetType,
};
use std::env;
use std::ffi::OsString;
Expand Down Expand Up @@ -85,7 +85,8 @@ impl CargoFuzzTestOptions {
}
ExecutionMode::ReplayCorpus(_)
| ExecutionMode::ReplayAllCrashes
| ExecutionMode::ReplayCrash(_) => {
| ExecutionMode::ReplayCrash(_)
| ExecutionMode::ListCrashIds(_) => {
self.check_centipede_binary_path_is_set()?;
self.check_corpus_db_is_set()?;
mode
Expand Down Expand Up @@ -278,6 +279,14 @@ impl FuzztestRunner {
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
}
Expand Down Expand Up @@ -687,4 +696,102 @@ mod tests {
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<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_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())
)));
}
}
139 changes: 75 additions & 64 deletions rust/cargo_fuzztest/tests/e2e_cli_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -159,30 +156,12 @@ 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")
Expand All @@ -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(&centipede_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(&centipede_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");
Expand All @@ -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 <crash_id> to verify it replays the crash.
// 3. Run cargo-fuzztest CLI with --replay-id <crash_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")
Expand Down Expand Up @@ -405,32 +386,62 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() {
);
}

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_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");

String::from_utf8_lossy(&process.stderr).to_string()
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("CENTIPEDE_BINARY_PATH")
.arg("--centipede-binary-path")
.arg(&centipede_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");

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(&centipede_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());
}
69 changes: 69 additions & 0 deletions rust/cargo_fuzztest/tests/runner_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,72 @@ fn test_runner_list_command() {
let args: Vec<String> = 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<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_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"));
}
Loading
Loading