Skip to content

Commit 65a3af7

Browse files
authored
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
1 parent ee2ef5b commit 65a3af7

5 files changed

Lines changed: 125 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cranelift/codegen/src/ir/trapcode.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ pub enum TrapCode {
5353
User(u16),
5454
}
5555

56+
impl TrapCode {
57+
/// Returns a slice of all traps except `TrapCode::User` traps
58+
pub const fn non_user_traps() -> &'static [TrapCode] {
59+
&[
60+
TrapCode::StackOverflow,
61+
TrapCode::HeapOutOfBounds,
62+
TrapCode::HeapMisaligned,
63+
TrapCode::TableOutOfBounds,
64+
TrapCode::IndirectCallToNull,
65+
TrapCode::BadSignature,
66+
TrapCode::IntegerOverflow,
67+
TrapCode::IntegerDivisionByZero,
68+
TrapCode::BadConversionToInteger,
69+
TrapCode::UnreachableCodeReached,
70+
TrapCode::Interrupt,
71+
]
72+
}
73+
}
74+
5675
impl Display for TrapCode {
5776
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
5877
use self::TrapCode::*;
@@ -102,24 +121,9 @@ mod tests {
102121
use super::*;
103122
use alloc::string::ToString;
104123

105-
// Everything but user-defined codes.
106-
const CODES: [TrapCode; 11] = [
107-
TrapCode::StackOverflow,
108-
TrapCode::HeapOutOfBounds,
109-
TrapCode::HeapMisaligned,
110-
TrapCode::TableOutOfBounds,
111-
TrapCode::IndirectCallToNull,
112-
TrapCode::BadSignature,
113-
TrapCode::IntegerOverflow,
114-
TrapCode::IntegerDivisionByZero,
115-
TrapCode::BadConversionToInteger,
116-
TrapCode::UnreachableCodeReached,
117-
TrapCode::Interrupt,
118-
];
119-
120124
#[test]
121125
fn display() {
122-
for r in &CODES {
126+
for r in TrapCode::non_user_traps() {
123127
let tc = *r;
124128
assert_eq!(tc.to_string().parse(), Ok(tc));
125129
}

cranelift/interpreter/src/step.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,7 @@ impl<'a, V> ControlFlow<'a, V> {
13851385
}
13861386
}
13871387

1388-
#[derive(Error, Debug, PartialEq)]
1388+
#[derive(Error, Debug, PartialEq, Eq, Hash)]
13891389
pub enum CraneliftTrap {
13901390
#[error("user code: {0}")]
13911391
User(TrapCode),

fuzz/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ cargo-fuzz = true
99

1010
[dependencies]
1111
anyhow = { workspace = true }
12+
once_cell = { workspace = true }
1213
cranelift-codegen = { workspace = true, features = ["incremental-cache"] }
1314
cranelift-reader = { workspace = true }
1415
cranelift-wasm = { workspace = true }

fuzz/fuzz_targets/cranelift-fuzzgen.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#![no_main]
22

33
use libfuzzer_sys::fuzz_target;
4+
use once_cell::sync::Lazy;
5+
use std::collections::HashMap;
6+
use std::sync::atomic::AtomicU64;
7+
use std::sync::atomic::Ordering;
48

59
use cranelift_codegen::data_value::DataValue;
6-
use cranelift_codegen::ir::LibCall;
10+
use cranelift_codegen::ir::{LibCall, TrapCode};
711
use cranelift_codegen::settings;
812
use cranelift_codegen::settings::Configurable;
913
use cranelift_filetests::function_runner::{TestFileCompiler, Trampoline};
@@ -19,6 +23,87 @@ use smallvec::smallvec;
1923

2024
const INTERPRETER_FUEL: u64 = 4096;
2125

26+
/// Gather statistics about the fuzzer executions
27+
struct Statistics {
28+
/// Inputs that fuzzgen can build a function with
29+
/// This is also how many compiles we executed
30+
pub valid_inputs: AtomicU64,
31+
32+
/// Total amount of runs that we tried in the interpreter
33+
/// One fuzzer input can have many runs
34+
pub total_runs: AtomicU64,
35+
/// How many runs were successful?
36+
/// This is also how many runs were run in the backend
37+
pub run_result_success: AtomicU64,
38+
/// How many runs resulted in a timeout?
39+
pub run_result_timeout: AtomicU64,
40+
/// How many runs ended with a trap?
41+
pub run_result_trap: HashMap<CraneliftTrap, AtomicU64>,
42+
}
43+
44+
impl Statistics {
45+
pub fn print(&self, valid_inputs: u64) {
46+
// We get valid_inputs as a param since we already loaded it previously.
47+
let total_runs = self.total_runs.load(Ordering::SeqCst);
48+
let run_result_success = self.run_result_success.load(Ordering::SeqCst);
49+
let run_result_timeout = self.run_result_timeout.load(Ordering::SeqCst);
50+
51+
println!("== FuzzGen Statistics ====================");
52+
println!("Valid Inputs: {}", valid_inputs);
53+
println!("Total Runs: {}", total_runs);
54+
println!(
55+
"Successful Runs: {} ({:.1}% of Total Runs)",
56+
run_result_success,
57+
(run_result_success as f64 / total_runs as f64) * 100.0
58+
);
59+
println!(
60+
"Timed out Runs: {} ({:.1}% of Total Runs)",
61+
run_result_timeout,
62+
(run_result_timeout as f64 / total_runs as f64) * 100.0
63+
);
64+
println!("Traps:");
65+
// Load and filter out empty trap codes.
66+
let mut traps = self
67+
.run_result_trap
68+
.iter()
69+
.map(|(trap, count)| (trap, count.load(Ordering::SeqCst)))
70+
.filter(|(_, count)| *count != 0)
71+
.collect::<Vec<_>>();
72+
73+
// Sort traps by count in a descending order
74+
traps.sort_by_key(|(_, count)| -(*count as i64));
75+
76+
for (trap, count) in traps.into_iter() {
77+
println!(
78+
"\t{}: {} ({:.1}% of Total Runs)",
79+
trap,
80+
count,
81+
(count as f64 / total_runs as f64) * 100.0
82+
);
83+
}
84+
}
85+
}
86+
87+
impl Default for Statistics {
88+
fn default() -> Self {
89+
// Pre-Register all trap codes since we can't modify this hashmap atomically.
90+
let mut run_result_trap = HashMap::new();
91+
run_result_trap.insert(CraneliftTrap::Debug, AtomicU64::new(0));
92+
run_result_trap.insert(CraneliftTrap::Resumable, AtomicU64::new(0));
93+
for trapcode in TrapCode::non_user_traps() {
94+
run_result_trap.insert(CraneliftTrap::User(*trapcode), AtomicU64::new(0));
95+
}
96+
97+
Self {
98+
valid_inputs: AtomicU64::new(0),
99+
total_runs: AtomicU64::new(0),
100+
run_result_success: AtomicU64::new(0),
101+
run_result_timeout: AtomicU64::new(0),
102+
run_result_trap,
103+
}
104+
}
105+
}
106+
22107
#[derive(Debug)]
23108
enum RunResult {
24109
Success(Vec<DataValue>),
@@ -79,7 +164,15 @@ fn build_interpreter(testcase: &TestCase) -> Interpreter {
79164
interpreter
80165
}
81166

167+
static STATISTICS: Lazy<Statistics> = Lazy::new(Statistics::default);
168+
82169
fuzz_target!(|testcase: TestCase| {
170+
// Periodically print statistics
171+
let valid_inputs = STATISTICS.valid_inputs.fetch_add(1, Ordering::SeqCst);
172+
if valid_inputs != 0 && valid_inputs % 10000 == 0 {
173+
STATISTICS.print(valid_inputs);
174+
}
175+
83176
// Native fn
84177
let flags = {
85178
let mut builder = settings::builder();
@@ -101,13 +194,18 @@ fuzz_target!(|testcase: TestCase| {
101194
let trampoline = compiled.get_trampoline(&testcase.func).unwrap();
102195

103196
for args in &testcase.inputs {
197+
STATISTICS.total_runs.fetch_add(1, Ordering::SeqCst);
198+
104199
// We rebuild the interpreter every run so that we don't accidentally carry over any state
105200
// between runs, such as fuel remaining.
106201
let mut interpreter = build_interpreter(&testcase);
107202
let int_res = run_in_interpreter(&mut interpreter, args);
108203
match int_res {
109-
RunResult::Success(_) => {}
110-
RunResult::Trap(_) => {
204+
RunResult::Success(_) => {
205+
STATISTICS.run_result_success.fetch_add(1, Ordering::SeqCst);
206+
}
207+
RunResult::Trap(trap) => {
208+
STATISTICS.run_result_trap[&trap].fetch_add(1, Ordering::SeqCst);
111209
// If this input traps, skip it and continue trying other inputs
112210
// for this function. We've already compiled it anyway.
113211
//
@@ -120,6 +218,7 @@ fuzz_target!(|testcase: TestCase| {
120218
RunResult::Timeout => {
121219
// We probably generated an infinite loop, we should drop this entire input.
122220
// We could `continue` like we do on traps, but timeouts are *really* expensive.
221+
STATISTICS.run_result_timeout.fetch_add(1, Ordering::SeqCst);
123222
return;
124223
}
125224
RunResult::Error(_) => panic!("interpreter failed: {:?}", int_res),

0 commit comments

Comments
 (0)