fuzzgen: Statistics framework (#4868)

* cranelift: Add non user trap codes function

* cranelift: Add Fuzzgen stats

* cranelift: Use `once_cell` and cleanup some stuff

* fuzzgen: Remove total_inputs metric

* fuzzgen: Filter empty trap codes
This commit is contained in:
Afonso Bordado
2022-09-27 17:04:57 +01:00
committed by GitHub
parent ee2ef5bdd0
commit 65a3af72c7
5 changed files with 125 additions and 20 deletions

1
Cargo.lock generated
View File

@@ -3591,6 +3591,7 @@ dependencies = [
"cranelift-reader", "cranelift-reader",
"cranelift-wasm", "cranelift-wasm",
"libfuzzer-sys", "libfuzzer-sys",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rand 0.8.5", "rand 0.8.5",

View File

@@ -53,6 +53,25 @@ pub enum TrapCode {
User(u16), User(u16),
} }
impl TrapCode {
/// Returns a slice of all traps except `TrapCode::User` traps
pub const fn non_user_traps() -> &'static [TrapCode] {
&[
TrapCode::StackOverflow,
TrapCode::HeapOutOfBounds,
TrapCode::HeapMisaligned,
TrapCode::TableOutOfBounds,
TrapCode::IndirectCallToNull,
TrapCode::BadSignature,
TrapCode::IntegerOverflow,
TrapCode::IntegerDivisionByZero,
TrapCode::BadConversionToInteger,
TrapCode::UnreachableCodeReached,
TrapCode::Interrupt,
]
}
}
impl Display for TrapCode { impl Display for TrapCode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use self::TrapCode::*; use self::TrapCode::*;
@@ -102,24 +121,9 @@ mod tests {
use super::*; use super::*;
use alloc::string::ToString; use alloc::string::ToString;
// Everything but user-defined codes.
const CODES: [TrapCode; 11] = [
TrapCode::StackOverflow,
TrapCode::HeapOutOfBounds,
TrapCode::HeapMisaligned,
TrapCode::TableOutOfBounds,
TrapCode::IndirectCallToNull,
TrapCode::BadSignature,
TrapCode::IntegerOverflow,
TrapCode::IntegerDivisionByZero,
TrapCode::BadConversionToInteger,
TrapCode::UnreachableCodeReached,
TrapCode::Interrupt,
];
#[test] #[test]
fn display() { fn display() {
for r in &CODES { for r in TrapCode::non_user_traps() {
let tc = *r; let tc = *r;
assert_eq!(tc.to_string().parse(), Ok(tc)); assert_eq!(tc.to_string().parse(), Ok(tc));
} }

View File

@@ -1385,7 +1385,7 @@ impl<'a, V> ControlFlow<'a, V> {
} }
} }
#[derive(Error, Debug, PartialEq)] #[derive(Error, Debug, PartialEq, Eq, Hash)]
pub enum CraneliftTrap { pub enum CraneliftTrap {
#[error("user code: {0}")] #[error("user code: {0}")]
User(TrapCode), User(TrapCode),

View File

@@ -9,6 +9,7 @@ cargo-fuzz = true
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
once_cell = { workspace = true }
cranelift-codegen = { workspace = true, features = ["incremental-cache"] } cranelift-codegen = { workspace = true, features = ["incremental-cache"] }
cranelift-reader = { workspace = true } cranelift-reader = { workspace = true }
cranelift-wasm = { workspace = true } cranelift-wasm = { workspace = true }

View File

@@ -1,9 +1,13 @@
#![no_main] #![no_main]
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use cranelift_codegen::data_value::DataValue; use cranelift_codegen::data_value::DataValue;
use cranelift_codegen::ir::LibCall; use cranelift_codegen::ir::{LibCall, TrapCode};
use cranelift_codegen::settings; use cranelift_codegen::settings;
use cranelift_codegen::settings::Configurable; use cranelift_codegen::settings::Configurable;
use cranelift_filetests::function_runner::{TestFileCompiler, Trampoline}; use cranelift_filetests::function_runner::{TestFileCompiler, Trampoline};
@@ -19,6 +23,87 @@ use smallvec::smallvec;
const INTERPRETER_FUEL: u64 = 4096; const INTERPRETER_FUEL: u64 = 4096;
/// Gather statistics about the fuzzer executions
struct Statistics {
/// Inputs that fuzzgen can build a function with
/// This is also how many compiles we executed
pub valid_inputs: AtomicU64,
/// Total amount of runs that we tried in the interpreter
/// One fuzzer input can have many runs
pub total_runs: AtomicU64,
/// How many runs were successful?
/// This is also how many runs were run in the backend
pub run_result_success: AtomicU64,
/// How many runs resulted in a timeout?
pub run_result_timeout: AtomicU64,
/// How many runs ended with a trap?
pub run_result_trap: HashMap<CraneliftTrap, AtomicU64>,
}
impl Statistics {
pub fn print(&self, valid_inputs: u64) {
// We get valid_inputs as a param since we already loaded it previously.
let total_runs = self.total_runs.load(Ordering::SeqCst);
let run_result_success = self.run_result_success.load(Ordering::SeqCst);
let run_result_timeout = self.run_result_timeout.load(Ordering::SeqCst);
println!("== FuzzGen Statistics ====================");
println!("Valid Inputs: {}", valid_inputs);
println!("Total Runs: {}", total_runs);
println!(
"Successful Runs: {} ({:.1}% of Total Runs)",
run_result_success,
(run_result_success as f64 / total_runs as f64) * 100.0
);
println!(
"Timed out Runs: {} ({:.1}% of Total Runs)",
run_result_timeout,
(run_result_timeout as f64 / total_runs as f64) * 100.0
);
println!("Traps:");
// Load and filter out empty trap codes.
let mut traps = self
.run_result_trap
.iter()
.map(|(trap, count)| (trap, count.load(Ordering::SeqCst)))
.filter(|(_, count)| *count != 0)
.collect::<Vec<_>>();
// Sort traps by count in a descending order
traps.sort_by_key(|(_, count)| -(*count as i64));
for (trap, count) in traps.into_iter() {
println!(
"\t{}: {} ({:.1}% of Total Runs)",
trap,
count,
(count as f64 / total_runs as f64) * 100.0
);
}
}
}
impl Default for Statistics {
fn default() -> Self {
// Pre-Register all trap codes since we can't modify this hashmap atomically.
let mut run_result_trap = HashMap::new();
run_result_trap.insert(CraneliftTrap::Debug, AtomicU64::new(0));
run_result_trap.insert(CraneliftTrap::Resumable, AtomicU64::new(0));
for trapcode in TrapCode::non_user_traps() {
run_result_trap.insert(CraneliftTrap::User(*trapcode), AtomicU64::new(0));
}
Self {
valid_inputs: AtomicU64::new(0),
total_runs: AtomicU64::new(0),
run_result_success: AtomicU64::new(0),
run_result_timeout: AtomicU64::new(0),
run_result_trap,
}
}
}
#[derive(Debug)] #[derive(Debug)]
enum RunResult { enum RunResult {
Success(Vec<DataValue>), Success(Vec<DataValue>),
@@ -79,7 +164,15 @@ fn build_interpreter(testcase: &TestCase) -> Interpreter {
interpreter interpreter
} }
static STATISTICS: Lazy<Statistics> = Lazy::new(Statistics::default);
fuzz_target!(|testcase: TestCase| { fuzz_target!(|testcase: TestCase| {
// Periodically print statistics
let valid_inputs = STATISTICS.valid_inputs.fetch_add(1, Ordering::SeqCst);
if valid_inputs != 0 && valid_inputs % 10000 == 0 {
STATISTICS.print(valid_inputs);
}
// Native fn // Native fn
let flags = { let flags = {
let mut builder = settings::builder(); let mut builder = settings::builder();
@@ -101,13 +194,18 @@ fuzz_target!(|testcase: TestCase| {
let trampoline = compiled.get_trampoline(&testcase.func).unwrap(); let trampoline = compiled.get_trampoline(&testcase.func).unwrap();
for args in &testcase.inputs { for args in &testcase.inputs {
STATISTICS.total_runs.fetch_add(1, Ordering::SeqCst);
// We rebuild the interpreter every run so that we don't accidentally carry over any state // We rebuild the interpreter every run so that we don't accidentally carry over any state
// between runs, such as fuel remaining. // between runs, such as fuel remaining.
let mut interpreter = build_interpreter(&testcase); let mut interpreter = build_interpreter(&testcase);
let int_res = run_in_interpreter(&mut interpreter, args); let int_res = run_in_interpreter(&mut interpreter, args);
match int_res { match int_res {
RunResult::Success(_) => {} RunResult::Success(_) => {
RunResult::Trap(_) => { STATISTICS.run_result_success.fetch_add(1, Ordering::SeqCst);
}
RunResult::Trap(trap) => {
STATISTICS.run_result_trap[&trap].fetch_add(1, Ordering::SeqCst);
// If this input traps, skip it and continue trying other inputs // If this input traps, skip it and continue trying other inputs
// for this function. We've already compiled it anyway. // for this function. We've already compiled it anyway.
// //
@@ -120,6 +218,7 @@ fuzz_target!(|testcase: TestCase| {
RunResult::Timeout => { RunResult::Timeout => {
// We probably generated an infinite loop, we should drop this entire input. // We probably generated an infinite loop, we should drop this entire input.
// We could `continue` like we do on traps, but timeouts are *really* expensive. // We could `continue` like we do on traps, but timeouts are *really* expensive.
STATISTICS.run_result_timeout.fetch_add(1, Ordering::SeqCst);
return; return;
} }
RunResult::Error(_) => panic!("interpreter failed: {:?}", int_res), RunResult::Error(_) => panic!("interpreter failed: {:?}", int_res),