From 0ca3fbab7b41eef9ed858220c73577530ba3e59f Mon Sep 17 00:00:00 2001 From: Ilya Yegorov Date: Thu, 17 Aug 2023 16:45:12 +0300 Subject: [PATCH] [casr-afl][casr-libfuzzer] Support timeout (#117) --- casr/src/bin/casr-afl.rs | 58 +++++++++++++++++++++++++++------- casr/src/bin/casr-gdb.rs | 17 ++++++++++ casr/src/bin/casr-java.rs | 22 ++++++++++--- casr/src/bin/casr-libfuzzer.rs | 25 +++++++++++++++ casr/src/bin/casr-python.rs | 22 ++++++++++--- casr/src/bin/casr-san.rs | 35 ++++++++++++++------ casr/src/bin/casr-ubsan.rs | 37 ++++------------------ casr/src/util.rs | 47 +++++++++++++++++++++++++-- docs/usage.md | 8 +++++ 9 files changed, 210 insertions(+), 61 deletions(-) diff --git a/casr/src/bin/casr-afl.rs b/casr/src/bin/casr-afl.rs index eae497e8..a83e0e96 100644 --- a/casr/src/bin/casr-afl.rs +++ b/casr/src/bin/casr-afl.rs @@ -32,8 +32,14 @@ impl<'a> AflCrashInfo { /// /// # Arguments /// - /// `output_dir` - save report to specified directory or use the same directory as crash - pub fn run_casr>>(&self, output_dir: T) -> anyhow::Result<()> { + /// * `output_dir` - save report to specified directory or use the same directory as crash + /// + /// * `timeout` - target program timeout (in seconds) + pub fn run_casr>>( + &self, + output_dir: T, + timeout: u64, + ) -> anyhow::Result<()> { let mut args: Vec = vec!["-o".to_string()]; let report_path = if let Some(out) = output_dir.into() { out.join(self.path.file_name().unwrap()) @@ -50,6 +56,9 @@ impl<'a> AflCrashInfo { args.push("--stdin".to_string()); args.push(self.path.to_str().unwrap().to_string()); } + if timeout != 0 { + args.append(&mut vec!["-t".to_string(), timeout.to_string()]); + } args.push("--".to_string()); args.extend_from_slice(&self.target_args); if let Some(at_index) = self.at_index { @@ -61,9 +70,12 @@ impl<'a> AflCrashInfo { let mut casr_cmd = Command::new(tool); casr_cmd.args(&args); debug!("{:?}", casr_cmd); + + // Get output let casr_output = casr_cmd .output() .with_context(|| format!("Couldn't launch {casr_cmd:?}"))?; + if !casr_output.status.success() { let err = String::from_utf8_lossy(&casr_output.stderr); if err.contains("Program terminated (no crash)") { @@ -97,6 +109,15 @@ fn main() -> Result<()> { .action(ArgAction::Set) .help("Number of parallel jobs for generating CASR reports [default: half of cpu cores]") .value_parser(clap::value_parser!(u32).range(1..))) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("input") .short('i') @@ -149,6 +170,7 @@ fn main() -> Result<()> { // Init log. util::initialize_logging(&matches); + // Get output dir let output_dir = matches.get_one::("output").unwrap(); if !output_dir.exists() { fs::create_dir_all(output_dir).with_context(|| { @@ -228,6 +250,14 @@ fn main() -> Result<()> { } } + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + + // Get number of threads let jobs = if let Some(jobs) = matches.get_one::("jobs") { *jobs as usize } else { @@ -245,13 +275,13 @@ fn main() -> Result<()> { custom_pool.install(|| { crashes .par_iter() - .try_for_each(|(_, crash)| crash.run_casr(output_dir.as_path())) + .try_for_each(|(_, crash)| crash.run_casr(output_dir.as_path(), timeout)) })?; // Deduplicate reports. if output_dir.read_dir()?.count() < 2 { info!("There are less than 2 CASR reports, nothing to deduplicate."); - return summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads); + return summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads, timeout); } info!("Deduplicating CASR reports..."); let casr_cluster_d = Command::new("casr-cluster") @@ -282,7 +312,7 @@ fn main() -> Result<()> { < 2 { info!("There are less than 2 CASR reports, nothing to cluster."); - return summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads); + return summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads, timeout); } info!("Clustering CASR reports..."); let casr_cluster_c = Command::new("casr-cluster") @@ -310,7 +340,7 @@ fn main() -> Result<()> { } } - summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads) + summarize_results(output_dir, &crashes, &gdb_argv, num_of_threads, timeout) } /// Copy crashes next to reports and print summary. @@ -318,15 +348,21 @@ fn main() -> Result<()> { /// /// # Arguments /// -/// `dir` - directory with casr reports -/// `crashes` - crashes info -/// `gdb_args` - run casr-gdb on uninstrumented binary if specified -/// `jobs` - number of threads for casr-gdb reports generation +/// * `dir` - directory with casr reports +/// +/// * `crashes` - crashes info +/// +/// * `gdb_args` - run casr-gdb on uninstrumented binary if specified +/// +/// * `jobs` - number of threads for casr-gdb reports generation +/// +/// * `timeout` - target program timeout fn summarize_results( dir: &Path, crashes: &HashMap, gdb_args: &Vec, jobs: usize, + timeout: u64, ) -> Result<()> { // Copy crashes next to reports copy_crashes(dir, crashes)?; @@ -358,7 +394,7 @@ fn summarize_results( at_index, is_asan: false, } - .run_casr(None) + .run_casr(None, timeout) }) })?; } diff --git a/casr/src/bin/casr-gdb.rs b/casr/src/bin/casr-gdb.rs index 74d3a1d6..1acafe50 100644 --- a/casr/src/bin/casr-gdb.rs +++ b/casr/src/bin/casr-gdb.rs @@ -60,6 +60,15 @@ fn main() -> Result<()> { .value_parser(clap::value_parser!(PathBuf)) .help("Stdin file for program"), ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("ignore") .long("ignore") @@ -84,6 +93,13 @@ fn main() -> Result<()> { bail!("Wrong arguments for starting program"); }; + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + init_ignored_frames!("cpp", "rust"); if let Some(path) = matches.get_one::("ignore") { util::add_custom_ignored_frames(path)?; @@ -161,6 +177,7 @@ fn main() -> Result<()> { let exectype = ExecType::Local(argv.as_slice()); let mut gdb_command = GdbCommand::new(&exectype); let gdb_command = gdb_command + .timeout(timeout) .stdin(&stdin_file) .r() .bt() diff --git a/casr/src/bin/casr-java.rs b/casr/src/bin/casr-java.rs index c1ae8a7f..1e69ebc1 100644 --- a/casr/src/bin/casr-java.rs +++ b/casr/src/bin/casr-java.rs @@ -6,7 +6,7 @@ use libcasr::java::*; use libcasr::report::CrashReport; use libcasr::stacktrace::*; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgGroup}; use regex::Regex; use std::path::PathBuf; @@ -48,6 +48,15 @@ fn main() -> Result<()> { .value_name("FILE") .help("Stdin file for program"), ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("ignore") .long("ignore") @@ -90,6 +99,13 @@ fn main() -> Result<()> { // Get stdin for target program. let stdin_file = util::stdin_from_matches(&matches)?; + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + // Run program. let mut java_cmd = Command::new(argv[0]); if let Some(ref file) = stdin_file { @@ -98,9 +114,7 @@ fn main() -> Result<()> { if argv.len() > 1 { java_cmd.args(&argv[1..]); } - let java_result = java_cmd - .output() - .with_context(|| "Couldn't run target program")?; + let java_result = util::get_output(&mut java_cmd, timeout, true)?; let java_stderr = String::from_utf8_lossy(&java_result.stderr); diff --git a/casr/src/bin/casr-libfuzzer.rs b/casr/src/bin/casr-libfuzzer.rs index 6721ab17..b6f37096 100644 --- a/casr/src/bin/casr-libfuzzer.rs +++ b/casr/src/bin/casr-libfuzzer.rs @@ -32,6 +32,15 @@ fn main() -> Result<()> { .action(ArgAction::Set) .help("Number of parallel jobs for generating CASR reports [default: half of cpu cores]") .value_parser(clap::value_parser!(u32).range(1..))) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("input") .short('i') @@ -83,8 +92,10 @@ fn main() -> Result<()> { // Init log. util::initialize_logging(&matches); + // Get input dir let input_dir = matches.get_one::("input").unwrap().as_path(); + // Get output dir let output_dir = matches.get_one::("output").unwrap(); if !output_dir.exists() { fs::create_dir_all(output_dir).with_context(|| { @@ -134,6 +145,14 @@ fn main() -> Result<()> { .filter(|(_, fname)| fname.starts_with("crash-") || fname.starts_with("leak-")) .collect(); + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + + // Get number of threads let jobs = if let Some(jobs) = matches.get_one::("jobs") { *jobs as usize } else { @@ -158,6 +177,9 @@ fn main() -> Result<()> { custom_pool.install(|| { crashes.par_iter().try_for_each(|(crash, fname)| { let mut casr_cmd = Command::new(tool); + if timeout != 0 { + casr_cmd.args(["-t".to_string(), timeout.to_string()]); + } casr_cmd.args([ "-o", format!("{}.casrep", output_dir.join(fname).display()).as_str(), @@ -170,9 +192,12 @@ fn main() -> Result<()> { casr_cmd.args(argv.clone()); casr_cmd.arg(crash); debug!("{:?}", casr_cmd); + + // Get output let casr_output = casr_cmd .output() .with_context(|| format!("Couldn't launch {casr_cmd:?}"))?; + if !casr_output.status.success() { let err = String::from_utf8_lossy(&casr_output.stderr); if err.contains("Program terminated (no crash)") { diff --git a/casr/src/bin/casr-python.rs b/casr/src/bin/casr-python.rs index 699baa71..a4102533 100644 --- a/casr/src/bin/casr-python.rs +++ b/casr/src/bin/casr-python.rs @@ -6,7 +6,7 @@ use libcasr::python::{PythonException, PythonStacktrace}; use libcasr::report::CrashReport; use libcasr::stacktrace::*; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgGroup}; use regex::Regex; use std::path::{Path, PathBuf}; @@ -47,6 +47,15 @@ fn main() -> Result<()> { .value_name("FILE") .help("Stdin file for program"), ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("ignore") .long("ignore") @@ -89,6 +98,13 @@ fn main() -> Result<()> { // Get stdin for target program. let stdin_file = util::stdin_from_matches(&matches)?; + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + // Run program. let mut python_cmd = Command::new(argv[0]); if let Some(ref file) = stdin_file { @@ -97,9 +113,7 @@ fn main() -> Result<()> { if argv.len() > 1 { python_cmd.args(&argv[1..]); } - let python_result = python_cmd - .output() - .with_context(|| "Couldn't run target program")?; + let python_result = util::get_output(&mut python_cmd, timeout, true)?; let python_stderr = String::from_utf8_lossy(&python_result.stderr); diff --git a/casr/src/bin/casr-san.rs b/casr/src/bin/casr-san.rs index e2ae3b69..5594b47f 100644 --- a/casr/src/bin/casr-san.rs +++ b/casr/src/bin/casr-san.rs @@ -60,6 +60,15 @@ fn main() -> Result<()> { .value_parser(clap::value_parser!(PathBuf)) .help("Stdin file for program"), ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution [default: disabled]") + .value_parser(clap::value_parser!(u64).range(1..)) + ) .arg( Arg::new("ignore") .long("ignore") @@ -92,6 +101,13 @@ fn main() -> Result<()> { // Get stdin for target program. let stdin_file = util::stdin_from_matches(&matches)?; + // Get timeout + let timeout = if let Some(timeout) = matches.get_one::("timeout") { + *timeout + } else { + 0 + }; + // Set rss limit. if let Ok(asan_options_str) = env::var("ASAN_OPTIONS") { let mut asan_options = asan_options_str.clone(); @@ -115,17 +131,15 @@ fn main() -> Result<()> { if argv.len() > 1 { sanitizers_cmd.args(&argv[1..]); } - let sanitizers_result = unsafe { - sanitizers_cmd - .pre_exec(|| { - if personality(linux_personality::ADDR_NO_RANDOMIZE).is_err() { - panic!("Cannot set personality"); - } - Ok(()) - }) - .output() - .with_context(|| "Couldn't run target program with sanitizers")? + let sanitizers_cmd = unsafe { + sanitizers_cmd.pre_exec(|| { + if personality(linux_personality::ADDR_NO_RANDOMIZE).is_err() { + panic!("Cannot set personality"); + } + Ok(()) + }) }; + let sanitizers_result = util::get_output(sanitizers_cmd, timeout, true)?; let sanitizers_stderr = String::from_utf8_lossy(&sanitizers_result.stderr); if sanitizers_stderr.contains("Cannot set personality") { @@ -207,6 +221,7 @@ fn main() -> Result<()> { // Get stack trace and mappings from gdb. let gdb_result = GdbCommand::new(&ExecType::Local(&argv)) + .timeout(timeout) .stdin(&stdin_file) .r() .bt() diff --git a/casr/src/bin/casr-ubsan.rs b/casr/src/bin/casr-ubsan.rs index 777e866c..ab005e65 100644 --- a/casr/src/bin/casr-ubsan.rs +++ b/casr/src/bin/casr-ubsan.rs @@ -1,4 +1,4 @@ -use casr::util::{initialize_logging, log_progress}; +use casr::util; use libcasr::report::CrashReport; use libcasr::severity::Severity; use libcasr::stacktrace::{CrashLine, CrashLineExt}; @@ -13,18 +13,16 @@ use clap::{ use log::{debug, info, warn}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use regex::Regex; -use wait_timeout::ChildExt; use walkdir::WalkDir; use std::collections::HashSet; use std::env; use std::fs; use std::fs::OpenOptions; -use std::io::{Read, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::RwLock; -use std::time::Duration; /// Extract ubsan warnings for specified input file /// @@ -68,31 +66,10 @@ fn extract_warnings( } debug!("Run: {:?}", cmd); + // Get output + let output = util::get_output(&mut cmd, timeout, false)?; // Get stderr - let stderr: String = - // If timeout is specified, spawn and check timeout - // Else get output - if timeout != 0 { - let mut child = cmd - .spawn() - .with_context(|| "Failed to start command: {cmd:?}")?; - if child - .wait_timeout(Duration::from_secs(timeout)) - .unwrap() - .is_none() - { - child.kill()?; - warn!("Timeout: {:?}", cmd); - } - let mut buf = vec![]; - let _ = child.stderr.unwrap().read_to_end(&mut buf); - String::from_utf8_lossy(&buf).to_string() - } else { - let output = cmd - .output() - .with_context(|| "Failed to start command: {cmd:?}")?; - String::from_utf8_lossy(&output.stderr).to_string() - }; + let stderr = String::from_utf8_lossy(&output.stderr); // Extract ubsan warnings let extracted_warnings = ubsan::extract_ubsan_warnings(&stderr); @@ -307,7 +284,7 @@ fn main() -> Result<()> { .get_matches(); // Init log. - initialize_logging(&matches); + util::initialize_logging(&matches); // Get input dir list let input_dirs: Vec<_> = matches.get_many::("input").unwrap().collect(); @@ -412,7 +389,7 @@ fn main() -> Result<()> { }) .collect() }, - || log_progress(&counter, total), + || util::log_progress(&counter, total), ); info!( diff --git a/casr/src/util.rs b/casr/src/util.rs index 04d46f1b..50e8103d 100644 --- a/casr/src/util.rs +++ b/casr/src/util.rs @@ -8,15 +8,18 @@ use libcasr::stacktrace::{ use anyhow::{bail, Context, Result}; use clap::ArgMatches; -use log::info; +use log::{info, warn}; use simplelog::*; use std::fs::OpenOptions; use std::io::Write; use std::io::{BufRead, BufReader}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Command, Output, Stdio}; use std::sync::RwLock; +use std::time::Duration; + +use wait_timeout::ChildExt; use which::which; /// Call sub tool with the provided options @@ -248,3 +251,43 @@ pub fn log_progress(processed_items: &RwLock, total: usize) { std::thread::sleep(std::time::Duration::from_millis(1000)); } } + +/// Get output of target command with specified timeout +/// +/// # Arguments +/// +/// * `command` - target command with args +/// +/// * `timeout` - target command timeout (in seconds) +/// +/// * `error_on_timeout` - throw an error if timeout happens +/// +/// # Return value +/// +/// Command output +pub fn get_output(command: &mut Command, timeout: u64, error_on_timeout: bool) -> Result { + // If timeout is specified, spawn and check timeout + // Else get output + if timeout != 0 { + let mut child = command + .spawn() + .with_context(|| "Failed to start command: {command:?}")?; + if child + .wait_timeout(Duration::from_secs(timeout)) + .unwrap() + .is_none() + { + let _ = child.kill(); + if error_on_timeout { + bail!("Timeout: {:?}", command); + } else { + warn!("Timeout: {:?}", command); + } + } + Ok(child.wait_with_output()?) + } else { + command + .output() + .with_context(|| format!("Couldn't launch {command:?}")) + } +} diff --git a/docs/usage.md b/docs/usage.md index e21029dc..11f1c47c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,6 +29,7 @@ Create CASR reports (.casrep) from gdb execution generated --stdout Print CASR report to stdout --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution [default: disabled] --ignore File with regular expressions for functions and file paths that should be ignored -h, --help Print help @@ -52,6 +53,7 @@ Create CASR reports (.casrep) from AddressSanitizer reports generated --stdout Print CASR report to stdout --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution [default: disabled] --ignore File with regular expressions for functions and file paths that should be ignored -h, --help Print help @@ -115,6 +117,7 @@ Create CASR reports (.casrep) from python reports generated --stdout Print CASR report to stdout --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution [default: disabled] --ignore File with regular expressions for functions and file paths that should be ignored --sub-tool Path to sub tool for crash analysis that will be called when main @@ -140,6 +143,7 @@ Create CASR reports (.casrep) from java reports generated --stdout Print CASR report to stdout --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution [default: disabled] --ignore File with regular expressions for functions and file paths that should be ignored --sub-tool Path to sub tool for crash analysis that will be called when main @@ -329,6 +333,8 @@ Triage crashes found by AFL++ debug] -j, --jobs Number of parallel jobs for generating CASR reports [default: half of cpu cores] + -t, --timeout Timeout (in seconds) for target execution [default: + disabled] -i, --input AFL++ work directory -o, --output Output directory with triaged reports --no-cluster Do not cluster CASR reports @@ -430,6 +436,8 @@ Triage crashes found by libFuzzer based fuzzer (C/C++/go-fuzz/Atheris/Jazzer) debug] -j, --jobs Number of parallel jobs for generating CASR reports [default: half of cpu cores] + -t, --timeout Timeout (in seconds) for target execution [default: + disabled] -i, --input Directory containing crashes found by libFuzzer [default: .] -o, --output Output directory with triaged reports