use crate::{
Build, Common, Cover, Fuzz, FuzzingEngines, Minimize,
util::{Context, ContextView},
};
use anyhow::{Context as _, Error, Result, anyhow, bail};
use console::{Term, style};
use glob::glob;
use std::{
fmt,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
process::{self, Stdio},
sync::{Arc, Mutex},
thread,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use strip_ansi_escapes::strip_str;
impl Fuzz {
fn output_paths(&self, cx: &Context) -> OutputPaths {
let output_target = if let Some(path) = self.binary.as_ref() {
if let Some(name) = path.file_prefix() {
format!("{}/{}", self.ziggy_output.display(), name.display())
} else {
self.ziggy_output.display().to_string()
}
} else {
format!("{}/{}", self.ziggy_output.display(), cx.bin_target)
};
let corpus = self
.corpus
.display()
.to_string()
.replace("{ziggy_output}", &self.ziggy_output.display().to_string())
.replace("{target_name}", &cx.bin_target);
let corpus_tmp = format!("{output_target}/corpus_tmp/");
let corpus_minimized = format!("{output_target}/corpus_minimized/");
OutputPaths {
corpus,
corpus_tmp,
corpus_minimized,
output_target,
}
}
pub fn afl(&self) -> bool {
!self.no_afl
}
pub fn honggfuzz(&self) -> bool {
if self.binary.is_some() {
false
} else if self.no_afl {
true
} else {
!self.no_honggfuzz && self.jobs > 1
}
}
pub fn fuzz(&mut self, common: &Common) -> Result<(), anyhow::Error> {
let cx = if let Some(binary) = self.binary.as_ref() {
self.coverage_worker = false;
if !binary.is_file() {
bail!("file not found `{}`", binary.display());
}
Context {
target_dir: "target".into(),
bin_target: binary.display().to_string(),
}
} else {
Context::new(common, self.target.clone())?
};
let cx_view = cx.view(common);
if self.binary.is_none() {
let build = Build {
no_afl: !self.afl(),
no_honggfuzz: !self.honggfuzz(),
release: self.release,
asan: self.asan,
target: self.target.clone(),
};
build.build(common).context("Failed to build the fuzzers")?;
}
let paths = self.output_paths(&cx);
let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
let crash_dir = format!("{}/crashes/{time}", paths.output_target);
let crash_path = Path::new(&crash_dir);
let timeouts_dir = format!("{}/timeouts/{time}", paths.output_target);
let timeouts_path = Path::new(&timeouts_dir);
fs::create_dir_all(crash_path)?;
fs::create_dir_all(timeouts_path)?;
fs::create_dir_all(format!("{}/logs", paths.output_target))?;
fs::create_dir_all(format!("{}/queue", paths.output_target))?;
if Path::new(&paths.corpus).exists() {
if self.minimize {
fs::create_dir_all(&paths.corpus_tmp)
.context("Could not create temporary corpus")?;
paths
.copy_corpora()
.context("Could not move all seeds to temporary corpus")?;
let _ = fs::remove_dir_all(&paths.corpus_minimized);
self.run_minimization(common, &paths)
.context("Failure while minimizing")?;
fs::remove_dir_all(&paths.corpus).context("Could not remove shared corpus")?;
fs::rename(&paths.corpus_minimized, &paths.corpus)
.context("Could not move minimized corpus over")?;
fs::remove_dir_all(&paths.corpus_tmp)
.context("Could not remove temporary corpus")?;
}
} else {
fs::create_dir_all(&paths.corpus)?;
}
let is_empty = fs::read_dir(&paths.corpus)?.next().is_none(); if is_empty {
let mut initial_corpus = File::create(format!("{}/init", &paths.corpus))?;
writeln!(&mut initial_corpus, "00000000")?;
drop(initial_corpus);
}
let mut processes = self.spawn_new_fuzzers(cx_view, &paths)?;
self.start_time = Instant::now();
let mut last_corpus_sync: Option<SystemTime> = None;
let mut last_sync_time = Instant::now();
let mut afl_output_ok = false;
if self.no_afl && self.coverage_worker {
bail!("cannot use --no-afl with --coverage-worker!");
}
if self.coverage_worker {
Cover::clean_old_cov(&cx)?;
Cover::build_runner(common)?;
}
let cov_start_time = Arc::new(Mutex::new(None));
let cov_end_time = Arc::new(Mutex::new(Instant::now()));
let coverage_now_running = Arc::new(Mutex::new(false));
let workspace_root = if self.binary.is_none() && self.coverage_worker {
common
.metadata()
.map(|m| m.workspace_root.to_string())
.unwrap_or_default()
} else {
String::default()
};
let main_corpus = &paths.corpus;
let output_target = &paths.output_target;
let mut stats = Stats::default();
common.shutdown_deferred(); loop {
let sleep_duration = Duration::from_secs(1);
thread::sleep(sleep_duration);
if common.is_terminated() {
eprintln!("\rShutting down...");
let res = [
stop_fuzzers(&processes),
self.sync_corpora(&paths, last_corpus_sync).map(|_| ()),
paths.sync_crashes(&cx, crash_path),
paths.sync_timeouts(&cx, timeouts_path),
];
return res.into_iter().fold(Ok(()), std::result::Result::and);
}
let coverage_status = match (
self.coverage_worker,
*coverage_now_running.lock().unwrap(),
cov_end_time.lock().unwrap().elapsed().as_secs() / 60,
) {
(true, false, wait) if wait < self.coverage_interval => {
format!("waiting {} minutes", self.coverage_interval - wait)
}
(true, false, _) => String::from("starting"),
(true, true, _) => String::from("running"),
(false, _, _) => String::from("disabled"),
};
let current_stats = self.print_stats(cx_view, &paths, &coverage_status);
if coverage_status.as_str() == "starting" {
*coverage_now_running.lock().unwrap() = true;
{
let main_corpus = main_corpus.clone();
let target = cx.bin_target.clone();
let workspace_root = workspace_root.clone();
let output_target = output_target.clone();
let cov_start_time = Arc::clone(&cov_start_time);
let cov_end_time = Arc::clone(&cov_end_time);
let coverage_now_running = Arc::clone(&coverage_now_running);
let cx = cx.clone();
thread::spawn(move || {
let mut seen_new_entry = false;
let prev_start_time =
cov_start_time.lock().unwrap().replace(SystemTime::now());
let profile_bin = cx.target_dir.join(format!("coverage/debug/{target}"));
let profile_base = cx
.target_dir
.join("coverage/debug/deps/coverage-")
.as_std_path()
.to_path_buf();
let entries = std::fs::read_dir(&main_corpus).unwrap();
for entry in entries.flatten().map(|e| e.path()) {
let potentially_new = entry.metadata().is_ok_and(|meta| {
meta.modified()
.ok()
.and_then(|mtime| {
prev_start_time
.map(|prev| prev < mtime || mtime.elapsed().is_err()) })
.unwrap_or(true)
});
if potentially_new && let Some(hash) = entry.file_name() {
let profile_file = {
let mut p = profile_base.join(hash);
p.add_extension("profraw");
p
};
if !profile_file.exists() {
let _ = process::Command::new(&profile_bin)
.arg(entry)
.env("LLVM_PROFILE_FILE", &profile_file)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
seen_new_entry = true;
}
}
}
let res = if seen_new_entry {
let coverage_dir = output_target + "/coverage";
let _ = fs::remove_dir_all(&coverage_dir);
Cover::run_grcov(&cx, "html", &coverage_dir, &workspace_root, Some(1))
} else {
Ok(())
};
{
let mut guard = coverage_now_running.lock().unwrap();
res.unwrap();
*guard = false;
}
*cov_end_time.lock().unwrap() = Instant::now();
});
}
}
if !afl_output_ok
&& let Ok(afl_log) =
fs::read_to_string(format!("{}/logs/afl.log", &paths.output_target))
{
if afl_log.contains("ready to roll") {
afl_output_ok = true;
} else if afl_log.contains("/proc/sys/kernel/core_pattern")
|| afl_log.contains("/sys/devices/system/cpu")
{
stop_fuzzers(&processes)?;
eprintln!(
"We highly recommend you configure your system for better performance:\n"
);
eprintln!(" cargo afl system-config\n");
eprintln!(
"Or set AFL_SKIP_CPUFREQ and AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES\n"
);
return Ok(());
}
}
if current_stats.crashes != stats.crashes {
paths.sync_crashes(&cx, crash_path)?;
}
if current_stats.timeouts != stats.timeouts {
paths.sync_timeouts(&cx, timeouts_path)?;
}
stats = current_stats;
if last_sync_time.elapsed() > Duration::from_mins(self.corpus_sync_interval) {
last_corpus_sync.replace(self.sync_corpora(&paths, last_corpus_sync)?);
last_sync_time = Instant::now();
}
if !processes
.iter_mut()
.all(|p| p.try_wait().is_ok_and(|exited| exited.is_none()))
{
stop_fuzzers(&processes)?;
return Ok(());
}
}
}
fn sync_corpora(
&self,
paths: &OutputPaths,
last_sync: Option<SystemTime>,
) -> Result<SystemTime, anyhow::Error> {
let now = SystemTime::now();
let afl_files = glob(&format!(
"{}/afl/mainaflfuzzer/queue/*",
paths.output_target,
))?
.flatten()
.zip(std::iter::repeat(Fuzzer::Afl));
let hfuzz_files = glob(&format!("{}/honggfuzz/corpus/*", paths.output_target))?
.flatten()
.zip(std::iter::repeat(Fuzzer::Honggfuzz));
let new_files = afl_files.chain(hfuzz_files).filter(|(file, _)| {
if let Ok(metadata) = file.metadata() {
if metadata.is_dir() {
return false;
}
if let Ok(mtime) = metadata.modified() {
last_sync.is_none_or(|last_sync| last_sync < mtime || mtime.elapsed().is_err())
} else {
true
}
} else {
false
}
});
let queue_path = PathBuf::from(format!("{}/queue", paths.output_target));
let corpus_path = PathBuf::from(format!("{}/corpus", paths.output_target));
for (file, fuzzer) in new_files {
if matches!(fuzzer, Fuzzer::Afl)
&& self.honggfuzz()
&& let Some(file_name) = file.file_name()
{
let target = queue_path.join(file_name);
if !target.exists() {
let _ = fs::copy(&file, target);
}
}
if let Ok(hash) = crate::util::hash_file(&file) {
let target_path = |hash| corpus_path.join(format!("{hash:x}"));
let target = target_path(hash);
if !target.exists() {
let _ = fs::copy(&file, &target);
} else {
let _ = resolve_collision(&file, hash, target_path);
}
}
}
return Ok(now);
#[derive(Clone, Copy, Debug)]
enum Fuzzer {
Afl,
Honggfuzz,
}
#[cold]
fn resolve_collision(
new_seed: &Path,
mut hash: u64,
target: impl Fn(u64) -> PathBuf,
) -> Result<(), anyhow::Error> {
let content = fs::read(new_seed)?;
for i in 0..1024 {
let target_path = target(hash);
let different_files = target_path
.metadata()
.is_ok_and(|m| m.len() != content.len() as u64)
|| content != fs::read(&target_path)?;
if target_path.exists() {
if !different_files {
return Ok(());
}
hash = hash.wrapping_add(i);
} else {
let _ = fs::copy(new_seed, &target_path);
return Ok(());
}
}
Ok(())
}
}
pub fn spawn_new_fuzzers(
&self,
cx: ContextView,
paths: &OutputPaths,
) -> Result<Vec<process::Child>, anyhow::Error> {
if self.no_afl && self.no_honggfuzz {
bail!("Pick at least one fuzzer.\nNote: -b/--binary implies --no-honggfuzz");
}
let mut fuzzer_handles = vec![];
let (afl_jobs, honggfuzz_jobs) = {
if self.no_afl {
(0, self.jobs)
} else if self.no_honggfuzz || self.binary.is_some() {
(self.jobs, 0)
} else {
let hfuzz = ((self.jobs + 1) / 3).min(4);
(self.jobs - hfuzz, hfuzz)
}
};
if honggfuzz_jobs > 4 {
eprintln!("Warning: running more honggfuzz jobs than 4 is not effective");
}
if afl_jobs > 0 {
std::fs::create_dir_all(format!("{}/afl", paths.output_target))?;
let afl_modes = [
"explore", "fast", "coe", "lin", "quad", "exploit", "rare", "explore", "fast",
"mmopt",
];
for job_num in 0..afl_jobs {
let is_main_instance = job_num == 0;
let fuzzer_name = match is_main_instance {
true => String::from("-Mmainaflfuzzer"),
false => format!("-Ssecondaryfuzzer{job_num}"),
};
let use_shared_corpus = match (self.no_honggfuzz, job_num) {
(false, 0) => format!("-F{}", &paths.corpus),
_ => String::new(),
};
let use_initial_corpus_dir = match (&self.initial_corpus, job_num) {
(Some(initial_corpus), 0) => {
format!("-F{}", &initial_corpus.display().to_string())
}
_ => String::new(),
};
let mopt_mutator = match job_num % 10 {
9 => "-L0",
_ => "",
};
let power_schedule = afl_modes
.get(job_num as usize % afl_modes.len())
.unwrap_or(&"fast");
let old_queue_cycling = match job_num % 10 {
8 => "-Z",
_ => "",
};
let cmplog_options = match job_num {
1 => "-l2a",
3 => "-l1",
14 => "-l2a",
22 => "-l3at",
_ => "-c-", };
let timeout_option_afl = match self.timeout {
Some(t) => format!("-t{}", t * 1000),
None => String::new(),
};
let memory_option_afl = match &self.memory_limit {
Some(m) => format!("-m{m}"),
None => String::new(),
};
let dictionary_option = match &self.dictionary {
Some(d) => format!("-x{}", &d.display().to_string()),
None => String::new(),
};
let mutation_option = match job_num / 5 {
0..=1 => "-P600",
2..=3 => "-Pexplore",
_ => "-Pexploit",
};
let input_format_option = self.config.input_format_flag();
let log_destination = match job_num {
0 => Some(File::create(format!(
"{}/logs/afl.log",
paths.output_target
))?),
1 => Some(File::create(format!(
"{}/logs/afl_1.log",
paths.output_target
))?),
_ => None,
};
let final_sync = match job_num {
0 => "AFL_FINAL_SYNC",
_ => "_DUMMY_VAR",
};
let target_path = self.binary.clone().unwrap_or_else(|| {
if self.release {
cx.target_dir()
.join(format!("afl/release/{}", cx.bin_target()))
.into_std_path_buf()
} else if self.asan && job_num == 0 {
cx.target_dir()
.join(format!(
"afl/{}/debug/{}",
target_triple::TARGET,
cx.bin_target()
))
.into_std_path_buf()
} else {
cx.target_dir()
.join(format!("afl/debug/{}", cx.bin_target()))
.into_std_path_buf()
}
});
let mut afl_flags = self.afl_flags.clone();
if is_main_instance {
for path in &self.foreign_sync_dirs {
afl_flags.push(format!("-F {}", path.display()));
}
}
fuzzer_handles.push(
cx.common()
.cargo()
.args(
[
"afl",
"fuzz",
&fuzzer_name,
&format!("-i{}", paths.corpus),
&format!("-p{power_schedule}"),
&format!("-o{}/afl", paths.output_target),
&format!("-g{}", self.min_length),
&format!("-G{}", self.max_length),
&use_shared_corpus,
&use_initial_corpus_dir,
old_queue_cycling,
cmplog_options,
mopt_mutator,
mutation_option,
input_format_option,
&timeout_option_afl,
&memory_option_afl,
&dictionary_option,
]
.iter()
.filter(|a| !a.is_empty()),
)
.args(afl_flags)
.arg(target_path)
.env("AFL_AUTORESUME", "1")
.env("AFL_TESTCACHE_SIZE", "100")
.env("AFL_FAST_CAL", "1")
.env("AFL_FORCE_UI", "1")
.env("AFL_IGNORE_UNKNOWN_ENVS", "1")
.env("AFL_CMPLOG_ONLY_NEW", "1")
.env("AFL_DISABLE_TRIM", "1")
.env("AFL_NO_WARN_INSTABILITY", "1")
.env("AFL_FUZZER_STATS_UPDATE_INTERVAL", "10")
.env("AFL_IMPORT_FIRST", "1")
.env(final_sync, "1")
.env("AFL_IGNORE_SEED_PROBLEMS", "1")
.env("AFL_PIZZA_MODE", "-1")
.stdout(
log_destination
.as_ref()
.map(std::fs::File::try_clone)
.transpose()?
.map_or_else(process::Stdio::null, Into::into),
)
.stderr(log_destination.map_or_else(process::Stdio::null, Into::into))
.spawn()?,
);
}
eprintln!("{} afl ", style(" Launched").green().bold());
}
if honggfuzz_jobs > 0 {
let run_args = {
let mut run_args = String::new();
run_args.push_str(&format!(" --input={}", paths.corpus));
run_args.push_str(&format!(" -o{}/honggfuzz/corpus", paths.output_target));
run_args.push_str(&format!(" -n{honggfuzz_jobs}"));
run_args.push_str(&format!(" -F{}", self.max_length));
run_args.push_str(&format!(" --dynamic_input={}/queue", paths.output_target));
run_args.push_str(" --tmout_sigvtalrm");
if let Some(t) = self.timeout {
run_args.push_str(&format!(" -t{t}"));
}
if let Some(d) = &self.dictionary {
run_args.push_str(&format!(" -w{}", d.display()));
}
if let Some(m) = &self.memory_limit {
run_args.push_str(&format!(" --rlimit_as={m}"));
}
run_args
};
let log = File::create(format!("{}/logs/honggfuzz.log", paths.output_target))?;
fuzzer_handles.push(
process::Command::new("script")
.args([
"--flush",
"--quiet",
"-c",
&format!(
"{} hfuzz run {}",
cx.common().cargo_path.display(),
&cx.bin_target()
),
"/dev/null",
])
.env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz")
.env("CARGO_TARGET_DIR", cx.target_dir().join("honggfuzz"))
.env(
"HFUZZ_WORKSPACE",
format!("{}/honggfuzz", paths.output_target),
)
.env("HFUZZ_RUN_ARGS", &run_args)
.stdin(std::process::Stdio::null())
.stderr(log.try_clone()?)
.stdout(log)
.spawn()?,
);
eprintln!(
"{} honggfuzz ",
style(" Launched").green().bold()
);
}
eprintln!("\nSee more live information by running:");
if afl_jobs > 0 {
eprintln!(
" {}",
style(format!("tail -f {}/logs/afl.log", paths.output_target)).bold()
);
}
if afl_jobs > 1 {
eprintln!(
" {}",
style(format!("tail -f {}/logs/afl_1.log", paths.output_target)).bold()
);
}
if honggfuzz_jobs > 0 {
eprintln!(
" {}",
style(format!(
"tail -f {}/logs/honggfuzz.log",
paths.output_target
))
.bold()
);
}
Ok(fuzzer_handles)
}
pub fn run_minimization(&self, common: &Common, paths: &OutputPaths) -> Result<()> {
let term = Term::stdout();
term.write_line(&format!(
"\n {}",
&style("Running minimization").magenta().bold()
))?;
let input_corpus = &paths.corpus_tmp;
let minimized_corpus = &paths.corpus_minimized;
let old_corpus_size = fs::read_dir(input_corpus).map_or_else(
|_| String::from("err"),
|corpus| format!("{}", corpus.count()),
);
let engine = match (self.no_afl, self.no_honggfuzz, self.jobs) {
(false, false, 1) => FuzzingEngines::AFLPlusPlus,
(false, false, _) => FuzzingEngines::All,
(false, true, _) => FuzzingEngines::AFLPlusPlus,
(true, false, _) => FuzzingEngines::Honggfuzz,
(true, true, _) => bail!("Pick at least one fuzzer"),
};
let minimization_args = Minimize {
target: self.target.clone(),
input_corpus: PathBuf::from(input_corpus),
output_corpus: PathBuf::from(minimized_corpus),
ziggy_output: self.ziggy_output.clone(),
jobs: self.jobs,
timeout: self.timeout.unwrap_or(5000),
engine,
};
match minimization_args.minimize(common) {
Ok(()) => {
let new_corpus_size = fs::read_dir(minimized_corpus).map_or_else(
|_| String::from("err"),
|corpus| format!("{}", corpus.count()),
);
term.move_cursor_up(1)?;
if new_corpus_size == *"err" || new_corpus_size == *"0" {
bail!(
"Please check the logs and make sure the right versions of the fuzzers are installed"
);
}
term.write_line(&format!(
"{} the corpus ({} -> {} files) \n",
style(" Minimized").magenta().bold(),
old_corpus_size,
new_corpus_size
))?;
}
Err(_) => {
bail!("Please check the logs, this might be an oom error");
}
}
Ok(())
}
pub fn print_stats(
&self,
cx: ContextView,
paths: &OutputPaths,
cov_worker_status: &str,
) -> Stats {
let fuzzer_name = format!(" {} ", cx.bin_target());
let reset = "\x1b[0m";
let gray = "\x1b[1;90m";
let red = "\x1b[1;91m";
let green = "\x1b[1;92m";
let yellow = "\x1b[1;93m";
let purple = "\x1b[1;95m";
let blue = "\x1b[1;96m";
let mut afl_status = format!("{green}running{reset} ─");
let mut afl_total_execs = String::new();
let mut afl_instances = String::new();
let mut afl_speed = String::new();
let mut afl_coverage = String::new();
let mut afl_crashes = String::new();
let mut afl_timeouts = String::new();
let mut afl_new_finds = String::new();
let mut afl_faves = String::new();
if !self.afl() {
afl_status = format!("{yellow}disabled{reset} ");
} else {
let afl_stats_process = cx
.common()
.cargo()
.args([
"afl",
"whatsup",
"-s",
&format!("{}/afl", paths.output_target),
])
.output();
if let Ok(process) = afl_stats_process {
let s = std::str::from_utf8(&process.stdout).unwrap_or_default();
for mut line in s.split('\n') {
line = line.trim();
if let Some(total_execs) = line.strip_prefix("Total execs : ") {
afl_total_execs =
String::from(total_execs.split(',').next().unwrap_or_default());
} else if let Some(instances) = line.strip_prefix("Fuzzers alive : ") {
afl_instances = String::from(instances);
} else if let Some(speed) = line.strip_prefix("Cumulative speed : ") {
afl_speed = String::from(speed);
} else if let Some(coverage) = line.strip_prefix("Coverage reached : ") {
afl_coverage = String::from(coverage);
} else if let Some(crashes) = line.strip_prefix("Crashes saved : ") {
afl_crashes = String::from(crashes);
} else if let Some(timeouts) = line.strip_prefix("Hangs saved : ") {
afl_timeouts = String::from(timeouts.split(' ').next().unwrap_or_default());
} else if let Some(new_finds) = line.strip_prefix("Time without finds : ") {
afl_new_finds =
String::from(new_finds.split(',').next().unwrap_or_default());
} else if let Some(pending_items) = line.strip_prefix("Pending items : ") {
afl_faves = String::from(
pending_items
.split(',')
.next()
.unwrap_or_default()
.strip_suffix(" faves")
.unwrap_or_default(),
);
}
}
}
}
let mut hf_status = format!("{green}running{reset} ─");
let mut hf_total_execs = String::new();
let mut hf_threads = String::new();
let mut hf_speed = String::new();
let mut hf_coverage = String::new();
let mut hf_crashes: usize = 0;
let mut hf_timeouts: usize = 0;
let mut hf_new_finds = String::new();
if !self.honggfuzz() {
hf_status = format!("{yellow}disabled{reset} ");
} else {
let hf_stats_process = process::Command::new("tail")
.args([
"-n300",
&format!("{}/logs/honggfuzz.log", paths.output_target),
])
.output();
if let Ok(process) = hf_stats_process {
let s = std::str::from_utf8(&process.stdout).unwrap_or_default();
for raw_line in s.split('\n') {
let stripped_line = strip_str(raw_line);
let line = stripped_line.trim();
if let Some(total_execs) = line.strip_prefix("Iterations : ") {
hf_total_execs =
String::from(total_execs.split(' ').next().unwrap_or_default());
} else if let Some(threads) = line.strip_prefix("Threads : ") {
hf_threads = String::from(threads.split(',').next().unwrap_or_default());
} else if let Some(speed) = line.strip_prefix("Speed : ") {
hf_speed = String::from(
speed
.split("[avg: ")
.nth(1)
.unwrap_or_default()
.strip_suffix(']')
.unwrap_or_default(),
) + "/sec";
} else if let Some(coverage) = line.strip_prefix("Coverage : ") {
hf_coverage = String::from(
coverage
.split('[')
.nth(1)
.unwrap_or_default()
.split(']')
.next()
.unwrap_or_default(),
);
} else if let Some(crashes) = line.strip_prefix("Crashes : ") {
hf_crashes = crashes
.split(' ')
.next()
.and_then(|n| n.parse().ok())
.unwrap_or_default();
} else if let Some(timeouts) = line.strip_prefix("Timeouts : ") {
hf_timeouts = timeouts
.split(' ')
.next()
.and_then(|n| n.parse().ok())
.unwrap_or_default();
} else if let Some(new_finds) = line.strip_prefix("Cov Update : ") {
hf_new_finds = String::from(new_finds.trim());
hf_new_finds = String::from(
hf_new_finds
.strip_prefix("0 days ")
.unwrap_or(&hf_new_finds),
);
hf_new_finds = String::from(
hf_new_finds
.strip_prefix("00 hrs ")
.unwrap_or(&hf_new_finds),
);
hf_new_finds = String::from(
hf_new_finds
.strip_prefix("00 mins ")
.unwrap_or(&hf_new_finds),
);
hf_new_finds = String::from(
hf_new_finds.strip_suffix(" ago").unwrap_or(&hf_new_finds),
);
}
}
}
}
let mut total_run_time = time_humanize::HumanTime::from(self.start_time.elapsed())
.to_text_en(
time_humanize::Accuracy::Rough,
time_humanize::Tense::Present,
);
if total_run_time == "now" {
total_run_time = String::from("...");
}
hf_crashes = hf_crashes.saturating_sub(hf_timeouts);
let mut screen = String::new();
screen += "\x1B[1;1H\x1B[2J";
screen += &format!(
"┌─ {blue}ziggy{reset} {purple}rocking{reset} ─────────{fuzzer_name:─^25.25}──────────────────{blue}/{red}////{reset}──┐\n"
);
screen += &format!(
"│{gray}run time :{reset} {total_run_time:17.17} {blue}/{red}///{reset} │\n"
);
screen += &format!(
"├─ {blue}afl++{reset} {afl_status:0}─────────────────────────────────────────────────────{blue}/{red}///{reset}─┤\n"
);
if !afl_status.contains("disabled") {
screen += &format!(
"│ {gray}instances :{reset} {afl_instances:17.17} │ {gray}best coverage :{reset} {afl_coverage:11.11} {blue}/{red}//{reset} │\n"
);
if afl_crashes == "0" {
screen += &format!(
"│{gray}cumulative speed :{reset} {afl_speed:17.17} │ {gray}crashes saved :{reset} {afl_crashes:11.11} {blue}/{red}/{reset} │\n"
);
} else {
screen += &format!(
"│{gray}cumulative speed :{reset} {afl_speed:17.17} │ {gray}crashes saved :{reset} {red}{afl_crashes:11.11}{reset} {blue}/{red}/{reset} │\n"
);
}
screen += &format!(
"│ {gray}total execs :{reset} {afl_total_execs:17.17} │{gray}timeouts saved :{reset} {afl_timeouts:17.17} │\n"
);
screen += &format!(
"│ {gray}top inputs todo :{reset} {afl_faves:17.17} │ {gray}no find for :{reset} {afl_new_finds:17.17} │\n"
);
}
screen += &format!(
"├─ {blue}honggfuzz{reset} {hf_status:0}─────────────────────────────────────────────────┬────┘\n"
);
if !hf_status.contains("disabled") {
screen += &format!(
"│ {gray}threads :{reset} {hf_threads:17.17} │ {gray}coverage :{reset} {hf_coverage:17.17} │\n"
);
if hf_crashes == 0 {
screen += &format!(
"│{gray}average speed :{reset} {hf_speed:17.17} │ {gray}crashes saved :{reset} {:17.17} │\n",
hf_crashes.to_string(),
);
} else {
screen += &format!(
"│{gray}average speed :{reset} {hf_speed:17.17} │ {gray}crashes saved :{reset} {red}{:17.17}{reset} │\n",
hf_crashes.to_string(),
);
}
screen += &format!(
"│ {gray}total execs :{reset} {hf_total_execs:17.17} │{gray}timeouts saved :{reset} {:17.17} │\n",
hf_timeouts.to_string(),
);
screen += &format!(
"│ │ {gray}no find for :{reset} {hf_new_finds:17.17} │\n"
);
}
if self.coverage_worker {
screen += &format!(
"├─ {blue}coverage{reset} {green}enabled{reset} ───────────┬───────────────────────────────────────┘\n"
);
screen += &format!("│{gray}status :{reset} {cov_worker_status:20.20} │\n");
screen += "└──────────────────────────────┘";
} else {
screen += "└──────────────────────────────────────────────────────────────────────┘\n";
}
eprintln!("{screen}");
Stats {
crashes: (afl_crashes, hf_crashes),
timeouts: (afl_timeouts, hf_timeouts),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, Debug)]
pub enum FuzzingConfig {
Generic,
Binary,
Text,
Blockchain,
}
impl FuzzingConfig {
fn input_format_flag(&self) -> &str {
match self {
Self::Text => "-atext",
Self::Binary => "-abinary",
_ => "",
}
}
}
impl fmt::Display for FuzzingConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
pub fn kill_subprocesses_recursively(pid: &str) -> Result<(), Error> {
let subprocesses = process::Command::new("pgrep")
.arg(format!("-P{pid}"))
.output()?;
for subprocess in std::str::from_utf8(&subprocesses.stdout)?.split('\n') {
if subprocess.is_empty() {
continue;
}
kill_subprocesses_recursively(subprocess)
.with_context(|| format!("Error in kill_subprocesses_recursively for pid {pid}"))?;
}
unsafe {
libc::kill(pid.parse::<i32>().unwrap(), libc::SIGTERM);
}
Ok(())
}
pub fn stop_fuzzers(processes: &[process::Child]) -> Result<(), Error> {
for process in processes {
kill_subprocesses_recursively(&process.id().to_string())?;
}
Ok(())
}
fn copy_from_dir(
crash_dir: PathBuf,
target_dir: &Path,
filter: impl Fn(&std::ffi::OsStr) -> bool,
) -> Result<(), anyhow::Error> {
if let Ok(crashes) = fs::read_dir(crash_dir) {
for crash_input in crashes.flatten() {
let file_name = crash_input.file_name();
let to_path = target_dir.join(&file_name);
if filter(&file_name) && !to_path.exists() {
fs::copy(crash_input.path(), to_path)?;
}
}
}
Ok(())
}
#[derive(Debug, Default)]
pub struct Stats {
crashes: (String, usize),
timeouts: (String, usize),
}
struct BStr<'a>(&'a std::ffi::OsStr);
impl BStr<'_> {
fn starts_with(&self, pat: &str) -> bool {
pat.len() <= self.0.len() && &self.0.as_encoded_bytes()[..pat.len()] == pat.as_bytes()
}
}
pub struct OutputPaths {
corpus: String,
corpus_tmp: String,
corpus_minimized: String,
output_target: String,
}
impl OutputPaths {
const HFUZZ_NO_CRASH: [&str; 3] = ["README.txt", "HONGGFUZZ.REPORT.TXT", "input"];
const HFUZZ_TIMEOUT_PREFIX: &str = "SIGVTALRM";
fn all_seeds(&self) -> Result<Vec<PathBuf>> {
Ok(glob(&format!("{}/afl/*/queue/*", self.output_target))
.map_err(|_| anyhow!("Failed to read AFL++ queue glob pattern"))?
.chain(
glob(&format!("{}/*", self.corpus))
.map_err(|_| anyhow!("Failed to read Honggfuzz corpus glob pattern"))?,
)
.flatten()
.filter(|f| f.is_file())
.collect())
}
pub fn copy_corpora(&self) -> Result<()> {
self.all_seeds()?.iter().for_each(|s| {
let _ = fs::copy(
s.to_str().unwrap_or_default(),
format!(
"{}/{}",
&self.corpus_tmp,
s.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
),
);
});
Ok(())
}
fn sync_crashes(&self, cx: &Context, target_dir: &Path) -> Result<(), anyhow::Error> {
for dir in glob(&format!("{}/afl/*/crashes", self.output_target))
.map_err(|_| anyhow!("Failed to read crashes glob pattern"))?
.flatten()
{
copy_from_dir(dir, target_dir, |_| true)?;
}
copy_from_dir(
PathBuf::from(format!(
"{}/honggfuzz/{}",
self.output_target, cx.bin_target
)),
target_dir,
|file_name| {
let no_timeout = !BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX);
no_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name)
},
)?;
Ok(())
}
fn sync_timeouts(&self, cx: &Context, target_dir: &Path) -> Result<(), anyhow::Error> {
for dir in glob(&format!("{}/afl/*/hangs", self.output_target))
.map_err(|_| anyhow!("Failed to read timeouts glob pattern"))?
.flatten()
{
copy_from_dir(dir, target_dir, |_| true)?;
}
copy_from_dir(
PathBuf::from(format!(
"{}/honggfuzz/{}",
self.output_target, cx.bin_target
)),
target_dir,
|file_name| {
let is_timeout = BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX);
is_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name)
},
)?;
Ok(())
}
}