Skip to content

Commit

Permalink
Optionaly show stats during fuzzing session
Browse files Browse the repository at this point in the history
  • Loading branch information
lukacan committed Mar 22, 2024
1 parent 2f866fe commit d0b375c
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ incremented upon a breaking change and the patch version will be incremented for
## [Unreleased]
### Added
- feat/possibility to implement custom transaction error handling ([#145](https://github.com/Ackee-Blockchain/trident/pull/145))
- feat/fuzzer-stats-logging, an optional stats output, triggered by `--stats` or `-s`, during fuzzing sessions ([#144](https://github.com/Ackee-Blockchain/trident/pull/144))
- feat/support of automatically obtaining fully qualified paths of Data Accounts Custom types for `accounts_snapshots.rs` ([#141](https://github.com/Ackee-Blockchain/trident/pull/141))
- feat/allow direct accounts manipulation and storage ([#142](https://github.com/Ackee-Blockchain/trident/pull/142))
- feat/support of non-corresponding instruction and context names ([#130](https://github.com/Ackee-Blockchain/trident/pull/130))
Expand Down
55 changes: 54 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions crates/cli/src/command/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub enum FuzzCommand {
/// Trident will return exit code 1 in case of found crash files in the crash folder. This is checked before and after the fuzz test run.
#[arg(short, long)]
with_exit_code: bool,
/// Trident will show statistics after the fuzzing session. It is recommended to turn `keep_output` on true.
#[arg(short, long)]
stats: bool,
},
/// Debug fuzz target with crash file
Run_Debug {
Expand Down Expand Up @@ -50,11 +53,12 @@ pub async fn fuzz(root: Option<String>, subcmd: FuzzCommand) {
FuzzCommand::Run {
target,
with_exit_code,
stats,
} => {
if with_exit_code {
commander.run_fuzzer_with_exit_code(target).await?;
commander.run_fuzzer_with_exit_code(target, stats).await?;
} else {
commander.run_fuzzer(target).await?;
commander.run_fuzzer(target, stats).await?;
}
}
FuzzCommand::Run_Debug {
Expand Down
1 change: 1 addition & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ pathdiff = "0.2.1"
solana-banks-client = "<1.18"
indicatif = "0.17.8"
regex = "1.10.3"
prettytable = "0.10.0"
13 changes: 13 additions & 0 deletions crates/client/derive/fuzz_test_executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream {
let variant_name = &variant.ident;
quote! {
#enum_name::#variant_name (ix) => {
#[cfg(fuzzing_with_stats)]
let mut stats_logger = FuzzingStatistics::new();
#[cfg(fuzzing_with_stats)]
stats_logger.increase_invoked(self.to_context_string());


let (mut signers, metas) = ix.get_accounts(client, &mut accounts.borrow_mut())
.map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string())))
.expect("Accounts calculation expect");
Expand Down Expand Up @@ -42,11 +48,16 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream {

match tx_result {
Ok(_) => {
#[cfg(fuzzing_with_stats)]
stats_logger.increase_successfully_invoked(self.to_context_string());

snaphot.capture_after(client).unwrap();
let (acc_before, acc_after) = snaphot.get_snapshot()
.map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string())))
.expect("Snapshot deserialization expect"); // we want to panic if we cannot unwrap to cause a crash

#[cfg(fuzzing_with_stats)]
stats_logger.output_serialized();
if let Err(e) = ix.check(acc_before, acc_after, data).map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string()))) {
eprintln!(
"CRASH DETECTED! Custom check after the {} instruction did not pass!",
Expand All @@ -55,6 +66,8 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream {
}
},
Err(e) => {
#[cfg(fuzzing_with_stats)]
stats_logger.output_serialized();
let mut raw_accounts = snaphot.get_raw_pre_ix_accounts();
ix.tx_error_handler(e, data, &mut raw_accounts)?
}
Expand Down
160 changes: 130 additions & 30 deletions crates/client/src/commander.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use tokio::{
};

use crate::constants::*;
use crate::fuzzing_stats::FuzzingStatistics;
use tokio::io::AsyncBufReadExt;

#[derive(Error, Debug)]
pub enum Error {
Expand Down Expand Up @@ -146,7 +148,7 @@ impl Commander {
}
/// Runs fuzzer on the given target with exit code option.
#[throws]
pub async fn run_fuzzer_with_exit_code(&self, target: String) {
pub async fn run_fuzzer_with_exit_code(&self, target: String, with_stats: bool) {
let config = Config::new();

// obtain hfuzz_run_args from env variable, this variable can contain multiple
Expand Down Expand Up @@ -181,26 +183,31 @@ impl Commander {
}
}

let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.arg("hfuzz")
.arg("run")
.arg(target)
.spawn()?;

tokio::select! {
res = child.wait() =>
match res {
Ok(status) => if !status.success() {
println!("Honggfuzz exited with an error!");
},
Err(_) => throw!(Error::FuzzingFailed),
},
_ = signal::ctrl_c() => {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
},
match with_stats {
true => {
let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.env("RUSTFLAGS", "--cfg fuzzing_with_stats")
.arg("hfuzz")
.arg("run")
.arg(target)
.stdout(Stdio::piped())
.spawn()?;
Self::handle_child_with_stats(&mut child).await?;
}
false => {
let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.arg("hfuzz")
.arg("run")
.arg(target)
.spawn()?;
Self::handle_child(&mut child).await?;
}
}

if let Ok(crash_files) = get_crash_files(&crash_dir, &ext) {
Expand All @@ -216,7 +223,7 @@ impl Commander {

/// Runs fuzzer on the given target.
#[throws]
pub async fn run_fuzzer(&self, target: String) {
pub async fn run_fuzzer(&self, target: String, with_stats: bool) {
let config = Config::new();

let hfuzz_run_args = std::env::var("HFUZZ_RUN_ARGS").unwrap_or_default();
Expand All @@ -228,15 +235,45 @@ impl Commander {

let fuzz_args = config.get_fuzz_args(hfuzz_run_args);

let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.arg("hfuzz")
.arg("run")
.arg(target)
.spawn()?;
match with_stats {
true => {
let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.env("RUSTFLAGS", "--cfg fuzzing_with_stats")
.arg("hfuzz")
.arg("run")
.arg(target)
.stdout(Stdio::piped())
.spawn()?;
Self::handle_child_with_stats(&mut child).await?;
}
false => {
let mut child = Command::new("cargo")
.env("HFUZZ_RUN_ARGS", fuzz_args)
.env("CARGO_TARGET_DIR", cargo_target_dir)
.env("HFUZZ_WORKSPACE", hfuzz_workspace)
.arg("hfuzz")
.arg("run")
.arg(target)
.spawn()?;
Self::handle_child(&mut child).await?;
}
}
}

/// Manages a child process in an async context, specifically for monitoring fuzzing tasks.
/// Waits for the process to exit or a Ctrl+C signal. Prints an error message if the process
/// exits with an error, and sleeps briefly on Ctrl+C. Throws `Error::FuzzingFailed` on errors.
///
/// # Arguments
/// * `child` - A mutable reference to a `Child` process.
///
/// # Errors
/// * Throws `Error::FuzzingFailed` if waiting on the child process fails.
#[throws]
async fn handle_child(child: &mut Child) {
tokio::select! {
res = child.wait() =>
match res {
Expand All @@ -250,6 +287,69 @@ impl Commander {
},
}
}
/// Asynchronously manages a child fuzzing process, collecting and logging its statistics.
/// This function spawns a new task dedicated to reading the process's standard output and logging the fuzzing statistics.
/// It waits for either the child process to exit or a Ctrl+C signal to be received. Upon process exit or Ctrl+C signal,
/// it stops the logging task and displays the collected statistics in a table format.
///
/// The implementation ensures that the statistics logging task only stops after receiving a signal indicating the end of the fuzzing process
/// or an interrupt from the user, preventing premature termination of the logging task if scenarios where reading is faster than fuzzing,
/// which should not be common.
///
/// # Arguments
/// * `child` - A mutable reference to a `Child` process, representing the child fuzzing process.
///
/// # Errors
/// * `Error::FuzzingFailed` - Thrown if there's an issue with managing the child process, such as failing to wait on the child process.
#[throws]
async fn handle_child_with_stats(child: &mut Child) {
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");

let reader = tokio::io::BufReader::new(stdout);

let fuzz_end = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let fuzz_end_clone = std::sync::Arc::clone(&fuzz_end);

tokio::spawn(async move {
let mut stats_logger = FuzzingStatistics::new();

let mut lines = reader.lines();
loop {
// Why the lock ??
// I`m not sure what happens if the fuzzing sessions is still active
// however the reader already read all the lines. I think that would bring us
// to the scenario where this reading task ends prematurely.
if fuzz_end_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
while let Ok(Some(line)) = lines.next_line().await {
stats_logger.insert_serialized(&line);
}
}
stats_logger.show_table();
});

tokio::select! {
res = child.wait() =>{
fuzz_end.store(true, std::sync::atomic::Ordering::SeqCst);
match res {
Ok(status) => {
if !status.success() {
println!("Honggfuzz exited with an error!");
}
},
Err(_) => throw!(Error::FuzzingFailed),
}
},
_ = signal::ctrl_c() => {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
fuzz_end.store(true, std::sync::atomic::Ordering::SeqCst);
},
}
}

/// Runs fuzzer on the given target.
#[throws]
Expand Down
Loading

0 comments on commit d0b375c

Please sign in to comment.