trident_client/commander/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
use fehler::{throw, throws};
use std::path::{Path, PathBuf};
use std::{io, process::Stdio, string::FromUtf8Error};
use thiserror::Error;
use tokio::{
io::AsyncWriteExt,
process::{Child, Command},
signal,
};
mod afl;
mod honggfuzz;
use tokio::io::AsyncBufReadExt;
use trident_fuzz::fuzz_stats::FuzzingStatistics;
#[derive(Error, Debug)]
pub enum Error {
#[error("{0:?}")]
Io(#[from] io::Error),
#[error("{0:?}")]
Utf8(#[from] FromUtf8Error),
#[error("build programs failed")]
BuildProgramsFailed,
#[error("fuzzing failed")]
FuzzingFailed,
#[error("Trident it not correctly initialized! The trident-tests folder in the root of your project does not exist")]
NotInitialized,
#[error("the crash file does not exist")]
CrashFileNotFound,
#[error("The Solana project does not contain any programs")]
NoProgramsFound,
#[error("Incorrect AFL workspace provided")]
BadAFLWorkspace,
}
/// `Commander` allows you to start localnet, build programs,
/// run tests and do other useful operations.
#[derive(Default)]
pub struct Commander {
root: PathBuf,
}
impl Commander {
/// Creates a new `Commander` instance with the provided `root`.
pub fn with_root(root: &PathBuf) -> Self {
Self {
root: Path::new(&root).to_path_buf(),
}
}
#[throws]
pub async fn build_anchor_project() {
let success = Command::new("anchor")
.arg("build")
.spawn()?
.wait()
.await?
.success();
if !success {
throw!(Error::BuildProgramsFailed);
}
}
/// Formats program code.
#[throws]
pub async fn format_program_code(code: &str) -> String {
let mut rustfmt = Command::new("rustfmt")
.args(["--edition", "2018"])
.kill_on_drop(true)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(stdio) = &mut rustfmt.stdin {
stdio.write_all(code.as_bytes()).await?;
}
let output = rustfmt.wait_with_output().await?;
String::from_utf8(output.stdout)?
}
/// Formats program code - nightly.
#[throws]
pub async fn format_program_code_nightly(code: &str) -> String {
let mut rustfmt = Command::new("rustfmt")
.arg("+nightly")
.arg("--config")
.arg(
"\
edition=2021,\
wrap_comments=true,\
normalize_doc_attributes=true",
)
.kill_on_drop(true)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(stdio) = &mut rustfmt.stdin {
stdio.write_all(code.as_bytes()).await?;
}
let output = rustfmt.wait_with_output().await?;
String::from_utf8(output.stdout)?
}
/// 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 {
Ok(status) => if !status.success() {
throw!(Error::FuzzingFailed);
},
Err(_) => throw!(Error::FuzzingFailed),
},
_ = signal::ctrl_c() => {
let _res = child.wait().await?;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
},
}
}
/// 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);
let stats_handle: tokio::task::JoinHandle<Result<FuzzingStatistics, std::io::Error>> =
tokio::spawn(async move {
let mut stats_logger = FuzzingStatistics::new();
let mut lines = reader.lines();
loop {
let _line = lines.next_line().await;
match _line {
Ok(__line) => match __line {
Some(content) => {
stats_logger.insert_serialized(&content);
}
None => {
if fuzz_end_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
}
},
Err(e) => return Err(e),
}
}
Ok(stats_logger)
});
tokio::select! {
res = child.wait() =>{
fuzz_end.store(true, std::sync::atomic::Ordering::SeqCst);
match res {
Ok(status) => {
if !status.success() {
throw!(Error::FuzzingFailed);
}
},
Err(_) => throw!(Error::FuzzingFailed),
}
},
_ = signal::ctrl_c() => {
fuzz_end.store(true, std::sync::atomic::Ordering::SeqCst);
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
},
}
let stats_result = stats_handle
.await
.expect("Unable to obtain Statistics Handle");
match stats_result {
Ok(stats_result) => {
stats_result.show_table();
}
Err(e) => {
println!("Statistics thread exited with the Error: {}", e);
}
}
}
}
fn get_crash_dir_and_ext(
root: &Path,
target: &str,
hfuzz_run_args: &str,
hfuzz_workspace: &str,
) -> (PathBuf, String) {
// FIXME: we split by whitespace without respecting escaping or quotes - same approach as honggfuzz-rs so there is no point to fix it here before the upstream is fixed
let hfuzz_run_args = hfuzz_run_args.split_whitespace();
let extension =
get_cmd_option_value(hfuzz_run_args.clone(), "-e", "--ext").unwrap_or("fuzz".to_string());
// If we run fuzzer like:
// HFUZZ_WORKSPACE="./new_hfuzz_workspace" HFUZZ_RUN_ARGS="--crashdir ./new_crash_dir -W ./new_workspace" cargo hfuzz run
// The structure will be as follows:
// ./new_hfuzz_workspace - will contain inputs
// ./new_crash_dir - will contain crashes
// ./new_workspace - will contain report
// So finally , we have to give precedence:
// --crashdir > --workspace > HFUZZ_WORKSPACE
let crash_dir = get_cmd_option_value(hfuzz_run_args.clone(), "", "--cr")
.or_else(|| get_cmd_option_value(hfuzz_run_args.clone(), "-W", "--w"));
let crash_path = if let Some(dir) = crash_dir {
// INFO If path is absolute, it replaces the current path.
root.join(dir)
} else {
std::path::Path::new(hfuzz_workspace).join(target)
};
(crash_path, extension)
}
fn get_cmd_option_value<'a>(
hfuzz_run_args: impl Iterator<Item = &'a str>,
short_opt: &str,
long_opt: &str,
) -> Option<String> {
let mut args_iter = hfuzz_run_args;
let mut value: Option<String> = None;
// ensure short option starts with one dash and long option with two dashes
let short_opt = format!("-{}", short_opt.trim_start_matches('-'));
let long_opt = format!("--{}", long_opt.trim_start_matches('-'));
while let Some(arg) = args_iter.next() {
match arg.strip_prefix(&short_opt) {
Some(val) if short_opt.len() > 1 => {
if !val.is_empty() {
// -ecrash for crash extension with no space
value = Some(val.to_string());
} else if let Some(next_arg) = args_iter.next() {
// -e crash for crash extension with space
value = Some(next_arg.to_string());
} else {
value = None;
}
}
_ => {
if arg.starts_with(&long_opt) && long_opt.len() > 2 {
value = args_iter.next().map(|a| a.to_string());
}
}
}
}
value
}
fn get_crash_files(
dir: &PathBuf,
extension: &str,
) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
let paths = std::fs::read_dir(dir)?
// Filter out all those directory entries which couldn't be read
.filter_map(|res| res.ok())
// Map the directory entries to paths
.map(|dir_entry| dir_entry.path())
// Filter out all paths with extensions other than `extension`
.filter_map(|path| {
if path.extension().map_or(false, |ext| ext == extension) {
Some(path)
} else {
None
}
})
.collect::<Vec<_>>();
Ok(paths)
}