Coverage Report

Created: 2025-10-12 07:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wasmtime/fuzz/fuzz_targets/cranelift-fuzzgen.rs
Line
Count
Source
1
#![no_main]
2
3
use cranelift_codegen::Context;
4
use cranelift_codegen::ir::Function;
5
use cranelift_codegen::ir::Signature;
6
use cranelift_codegen::ir::UserExternalName;
7
use cranelift_codegen::ir::UserFuncName;
8
use cranelift_control::ControlPlane;
9
use libfuzzer_sys::arbitrary;
10
use libfuzzer_sys::arbitrary::Arbitrary;
11
use libfuzzer_sys::arbitrary::Unstructured;
12
use libfuzzer_sys::fuzz_target;
13
use std::collections::HashMap;
14
use std::fmt;
15
use std::sync::LazyLock;
16
use std::sync::atomic::AtomicU64;
17
use std::sync::atomic::Ordering;
18
19
use cranelift_codegen::data_value::DataValue;
20
use cranelift_codegen::ir::{LibCall, TrapCode};
21
use cranelift_codegen::isa;
22
use cranelift_filetests::function_runner::{CompiledTestFile, TestFileCompiler, Trampoline};
23
use cranelift_fuzzgen::*;
24
use cranelift_interpreter::environment::FuncIndex;
25
use cranelift_interpreter::environment::FunctionStore;
26
use cranelift_interpreter::interpreter::{
27
    Interpreter, InterpreterError, InterpreterState, LibCallValues,
28
};
29
use cranelift_interpreter::step::ControlFlow;
30
use cranelift_interpreter::step::CraneliftTrap;
31
use cranelift_native::builder_with_options;
32
use smallvec::smallvec;
33
34
const INTERPRETER_FUEL: u64 = 4096;
35
36
/// Gather statistics about the fuzzer executions
37
struct Statistics {
38
    /// Inputs that fuzzgen can build a function with
39
    /// This is also how many compiles we executed
40
    pub valid_inputs: AtomicU64,
41
    /// How many times did we generate an invalid format?
42
    pub invalid_inputs: AtomicU64,
43
44
    /// Total amount of runs that we tried in the interpreter
45
    /// One fuzzer input can have many runs
46
    pub total_runs: AtomicU64,
47
    /// How many runs were successful?
48
    /// This is also how many runs were run in the backend
49
    pub run_result_success: AtomicU64,
50
    /// How many runs resulted in a timeout?
51
    pub run_result_timeout: AtomicU64,
52
    /// How many runs ended with a trap?
53
    pub run_result_trap: HashMap<CraneliftTrap, AtomicU64>,
54
}
55
56
impl Statistics {
57
0
    pub fn print(&self, valid_inputs: u64) {
58
        // We get valid_inputs as a param since we already loaded it previously.
59
0
        let total_runs = self.total_runs.load(Ordering::SeqCst);
60
0
        let invalid_inputs = self.invalid_inputs.load(Ordering::SeqCst);
61
0
        let run_result_success = self.run_result_success.load(Ordering::SeqCst);
62
0
        let run_result_timeout = self.run_result_timeout.load(Ordering::SeqCst);
63
64
0
        println!("== FuzzGen Statistics  ====================");
65
0
        println!("Valid Inputs: {valid_inputs}");
66
0
        println!(
67
0
            "Invalid Inputs: {} ({:.1}% of Total Inputs)",
68
            invalid_inputs,
69
0
            (invalid_inputs as f64 / (valid_inputs + invalid_inputs) as f64) * 100.0
70
        );
71
0
        println!("Total Runs: {total_runs}");
72
0
        println!(
73
0
            "Successful Runs: {} ({:.1}% of Total Runs)",
74
            run_result_success,
75
0
            (run_result_success as f64 / total_runs as f64) * 100.0
76
        );
77
0
        println!(
78
0
            "Timed out Runs: {} ({:.1}% of Total Runs)",
79
            run_result_timeout,
80
0
            (run_result_timeout as f64 / total_runs as f64) * 100.0
81
        );
82
0
        println!("Traps:");
83
        // Load and filter out empty trap codes.
84
0
        let mut traps = self
85
0
            .run_result_trap
86
0
            .iter()
87
0
            .map(|(trap, count)| (trap, count.load(Ordering::SeqCst)))
88
0
            .filter(|(_, count)| *count != 0)
89
0
            .collect::<Vec<_>>();
90
91
        // Sort traps by count in a descending order
92
0
        traps.sort_by_key(|(_, count)| -(*count as i64));
93
94
0
        for (trap, count) in traps.into_iter() {
95
0
            println!(
96
0
                "\t{}: {} ({:.1}% of Total Runs)",
97
0
                trap,
98
0
                count,
99
0
                (count as f64 / total_runs as f64) * 100.0
100
0
            );
101
0
        }
102
0
    }
103
}
104
105
impl Default for Statistics {
106
1
    fn default() -> Self {
107
        // Pre-Register all trap codes since we can't modify this hashmap atomically.
108
1
        let mut run_result_trap = HashMap::new();
109
1
        run_result_trap.insert(CraneliftTrap::Debug, AtomicU64::new(0));
110
1
        run_result_trap.insert(CraneliftTrap::BadSignature, AtomicU64::new(0));
111
1
        run_result_trap.insert(CraneliftTrap::UnreachableCodeReached, AtomicU64::new(0));
112
1
        run_result_trap.insert(CraneliftTrap::HeapMisaligned, AtomicU64::new(0));
113
5
        for trapcode in TrapCode::non_user_traps() {
114
5
            run_result_trap.insert(CraneliftTrap::User(*trapcode), AtomicU64::new(0));
115
5
        }
116
117
1
        Self {
118
1
            valid_inputs: AtomicU64::new(0),
119
1
            invalid_inputs: AtomicU64::new(0),
120
1
            total_runs: AtomicU64::new(0),
121
1
            run_result_success: AtomicU64::new(0),
122
1
            run_result_timeout: AtomicU64::new(0),
123
1
            run_result_trap,
124
1
        }
125
1
    }
126
}
127
128
#[derive(Debug)]
129
enum RunResult {
130
    Success(Vec<DataValue>),
131
    Trap(CraneliftTrap),
132
    Timeout,
133
    Error(Box<dyn std::error::Error>),
134
}
135
136
impl PartialEq for RunResult {
137
26.9k
    fn eq(&self, other: &Self) -> bool {
138
26.9k
        match (self, other) {
139
13.4k
            (RunResult::Success(l), RunResult::Success(r)) => {
140
117k
                l.len() == r.len() && l.iter().zip(r).all(|(l, r)| l.bitwise_eq(r))
141
            }
142
0
            (RunResult::Trap(l), RunResult::Trap(r)) => l == r,
143
0
            (RunResult::Timeout, RunResult::Timeout) => true,
144
0
            (RunResult::Error(_), RunResult::Error(_)) => unimplemented!(),
145
13.4k
            _ => false,
146
        }
147
26.9k
    }
148
}
149
150
pub struct TestCase {
151
    /// TargetIsa to use when compiling this test case
152
    pub isa: isa::OwnedTargetIsa,
153
    /// Functions under test
154
    /// By convention the first function is the main function.
155
    pub functions: Vec<Function>,
156
    /// Control planes for function compilation.
157
    /// There should be an equal amount as functions to compile.
158
    pub ctrl_planes: Vec<ControlPlane>,
159
    /// Generate multiple test inputs for each test case.
160
    /// This allows us to get more coverage per compilation, which may be somewhat expensive.
161
    pub inputs: Vec<TestCaseInput>,
162
    /// Should this `TestCase` be tested after optimizations.
163
    pub compare_against_host: bool,
164
}
165
166
impl fmt::Debug for TestCase {
167
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168
0
        if !self.compare_against_host {
169
0
            writeln!(f, ";; Testing against optimized version")?;
170
0
        }
171
0
        PrintableTestCase::run(&self.isa, &self.functions, &self.inputs).fmt(f)
172
0
    }
173
}
174
175
impl<'a> Arbitrary<'a> for TestCase {
176
7.27k
    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
177
7.27k
        let _ = env_logger::try_init();
178
7.27k
        Self::generate(u).map_err(|_| {
179
606
            STATISTICS.invalid_inputs.fetch_add(1, Ordering::SeqCst);
180
606
            arbitrary::Error::IncorrectFormat
181
606
        })
182
7.27k
    }
183
}
184
185
impl TestCase {
186
7.27k
    pub fn generate(u: &mut Unstructured) -> anyhow::Result<Self> {
187
7.27k
        let mut generator = FuzzGen::new(u);
188
189
7.27k
        let compare_against_host = generator.u.arbitrary()?;
190
191
        // TestCase is meant to be consumed by a runner, so we make the assumption here that we're
192
        // generating a TargetIsa for the host.
193
7.27k
        let mut builder =
194
7.27k
            builder_with_options(true).expect("Unable to build a TargetIsa for the current host");
195
7.27k
        let flags = generator.generate_flags(builder.triple().architecture)?;
196
7.27k
        generator.set_isa_flags(&mut builder, IsaFlagGen::Host)?;
197
7.27k
        let isa = builder.finish(flags)?;
198
199
        // When generating functions, we allow each function to call any function that has
200
        // already been generated. This guarantees that we never have loops in the call graph.
201
        // We generate these backwards, and then reverse them so that the main function is at
202
        // the start.
203
7.27k
        let func_count = generator
204
7.27k
            .u
205
7.27k
            .int_in_range(generator.config.testcase_funcs.clone())?;
206
7.27k
        let mut functions: Vec<Function> = Vec::with_capacity(func_count);
207
7.27k
        let mut ctrl_planes: Vec<ControlPlane> = Vec::with_capacity(func_count);
208
17.4k
        for i in (0..func_count).rev() {
209
            // Function name must be in a different namespace than TESTFILE_NAMESPACE (0)
210
17.4k
            let fname = UserFuncName::user(1, i as u32);
211
212
17.4k
            let usercalls: Vec<(UserExternalName, Signature)> = functions
213
17.4k
                .iter()
214
29.7k
                .map(|f| {
215
29.7k
                    (
216
29.7k
                        f.name.get_user().unwrap().clone(),
217
29.7k
                        f.stencil.signature.clone(),
218
29.7k
                    )
219
29.7k
                })
220
17.4k
                .collect();
221
222
17.4k
            let func = generator.generate_func(
223
17.4k
                fname,
224
17.4k
                isa.clone(),
225
17.4k
                usercalls,
226
17.4k
                ALLOWED_LIBCALLS.to_vec(),
227
606
            )?;
228
16.8k
            functions.push(func);
229
230
16.8k
            ctrl_planes.push(ControlPlane::arbitrary(generator.u)?);
231
        }
232
        // Now reverse the functions so that the main function is at the start.
233
6.67k
        functions.reverse();
234
235
6.67k
        let main = &functions[0];
236
6.67k
        let inputs = generator.generate_test_inputs(&main.signature)?;
237
238
6.67k
        Ok(TestCase {
239
6.67k
            isa,
240
6.67k
            functions,
241
6.67k
            ctrl_planes,
242
6.67k
            inputs,
243
6.67k
            compare_against_host,
244
6.67k
        })
245
7.27k
    }
246
247
327
    fn to_optimized(&self) -> Self {
248
327
        let mut ctrl_planes = self.ctrl_planes.clone();
249
327
        let optimized_functions: Vec<Function> = self
250
327
            .functions
251
327
            .iter()
252
327
            .zip(ctrl_planes.iter_mut())
253
1.03k
            .map(|(func, ctrl_plane)| {
254
1.03k
                let mut ctx = Context::for_function(func.clone());
255
1.03k
                ctx.optimize(self.isa.as_ref(), ctrl_plane).unwrap();
256
1.03k
                ctx.func
257
1.03k
            })
258
327
            .collect();
259
260
327
        TestCase {
261
327
            isa: self.isa.clone(),
262
327
            functions: optimized_functions,
263
327
            ctrl_planes,
264
327
            inputs: self.inputs.clone(),
265
327
            compare_against_host: false,
266
327
        }
267
327
    }
268
269
    /// Returns the main function of this test case.
270
6.34k
    pub fn main(&self) -> &Function {
271
6.34k
        &self.functions[0]
272
6.34k
    }
273
}
274
275
15.5k
fn run_in_interpreter(interpreter: &mut Interpreter, args: &[DataValue]) -> RunResult {
276
    // The entrypoint function is always 0
277
15.5k
    let index = FuncIndex::from_u32(0);
278
15.5k
    let res = interpreter.call_by_index(index, args);
279
280
15.0k
    match res {
281
14.8k
        Ok(ControlFlow::Return(results)) => RunResult::Success(results.to_vec()),
282
181
        Ok(ControlFlow::Trap(trap)) => RunResult::Trap(trap),
283
0
        Ok(cf) => RunResult::Error(format!("Unrecognized exit ControlFlow: {cf:?}").into()),
284
480
        Err(InterpreterError::FuelExhausted) => RunResult::Timeout,
285
0
        Err(e) => RunResult::Error(e.into()),
286
    }
287
15.5k
}
288
289
12.0k
fn run_in_host(
290
12.0k
    compiled: &CompiledTestFile,
291
12.0k
    trampoline: &Trampoline,
292
12.0k
    args: &[DataValue],
293
12.0k
) -> RunResult {
294
12.0k
    let res = trampoline.call(compiled, args);
295
12.0k
    RunResult::Success(res)
296
12.0k
}
297
298
/// These libcalls need a interpreter implementation in `build_interpreter`
299
const ALLOWED_LIBCALLS: &'static [LibCall] = &[
300
    LibCall::CeilF32,
301
    LibCall::CeilF64,
302
    LibCall::FloorF32,
303
    LibCall::FloorF64,
304
    LibCall::TruncF32,
305
    LibCall::TruncF64,
306
];
307
308
15.5k
fn build_interpreter(testcase: &TestCase) -> Interpreter<'_> {
309
15.5k
    let mut env = FunctionStore::default();
310
33.2k
    for func in testcase.functions.iter() {
311
33.2k
        env.add(func.name.to_string(), &func);
312
33.2k
    }
313
314
15.5k
    let state = InterpreterState::default()
315
15.5k
        .with_function_store(env)
316
243k
        .with_libcall_handler(|libcall: LibCall, args: LibCallValues| {
317
            use LibCall::*;
318
243k
            Ok(smallvec![match (libcall, &args[..]) {
319
0
                (CeilF32, [DataValue::F32(a)]) => DataValue::F32(a.ceil()),
320
0
                (CeilF64, [DataValue::F64(a)]) => DataValue::F64(a.ceil()),
321
0
                (FloorF32, [DataValue::F32(a)]) => DataValue::F32(a.floor()),
322
0
                (FloorF64, [DataValue::F64(a)]) => DataValue::F64(a.floor()),
323
0
                (TruncF32, [DataValue::F32(a)]) => DataValue::F32(a.trunc()),
324
0
                (TruncF64, [DataValue::F64(a)]) => DataValue::F64(a.trunc()),
325
0
                _ => unreachable!(),
326
            }])
327
243k
        });
328
329
15.5k
    let interpreter = Interpreter::new(state).with_fuel(Some(INTERPRETER_FUEL));
330
15.5k
    interpreter
331
15.5k
}
332
333
static STATISTICS: LazyLock<Statistics> = LazyLock::new(Statistics::default);
334
335
6.67k
fn run_test_inputs(testcase: &TestCase, run: impl Fn(&[DataValue]) -> RunResult) {
336
20.3k
    for args in &testcase.inputs {
337
14.1k
        STATISTICS.total_runs.fetch_add(1, Ordering::SeqCst);
338
339
        // We rebuild the interpreter every run so that we don't accidentally carry over any state
340
        // between runs, such as fuel remaining.
341
14.1k
        let mut interpreter = build_interpreter(&testcase);
342
14.1k
        let int_res = run_in_interpreter(&mut interpreter, args);
343
14.1k
        match int_res {
344
13.4k
            RunResult::Success(_) => {
345
13.4k
                STATISTICS.run_result_success.fetch_add(1, Ordering::SeqCst);
346
13.4k
            }
347
181
            RunResult::Trap(trap) => {
348
181
                STATISTICS.run_result_trap[&trap].fetch_add(1, Ordering::SeqCst);
349
                // If this input traps, skip it and continue trying other inputs
350
                // for this function. We've already compiled it anyway.
351
                //
352
                // We could catch traps in the host run and compare them to the
353
                // interpreter traps, but since we already test trap cases with
354
                // wasm tests and wasm-level fuzzing, the amount of effort does
355
                // not justify implementing it again here.
356
181
                continue;
357
            }
358
            RunResult::Timeout => {
359
                // We probably generated an infinite loop, we should drop this entire input.
360
                // We could `continue` like we do on traps, but timeouts are *really* expensive.
361
480
                STATISTICS.run_result_timeout.fetch_add(1, Ordering::SeqCst);
362
480
                return;
363
            }
364
0
            RunResult::Error(e) => panic!("interpreter failed: {e:?}"),
365
        }
366
367
13.4k
        let res = run(args);
368
369
        // This situation can happen when we are comparing the interpreter against the interpreter, and
370
        // one of the optimization passes has increased the number of instructions in the function.
371
        // This can cause the interpreter to run out of fuel in the second run, but not the first.
372
        // We should ignore these cases.
373
        // Running in the host should never return a timeout, so that should be ok.
374
13.4k
        if res == RunResult::Timeout {
375
0
            return;
376
13.4k
        }
377
378
13.4k
        assert_eq!(int_res, res);
379
    }
380
6.67k
}
cranelift_fuzzgen::run_test_inputs::<cranelift_fuzzgen::_::__libfuzzer_sys_run::{closure#2}>
Line
Count
Source
335
327
fn run_test_inputs(testcase: &TestCase, run: impl Fn(&[DataValue]) -> RunResult) {
336
1.74k
    for args in &testcase.inputs {
337
1.43k
        STATISTICS.total_runs.fetch_add(1, Ordering::SeqCst);
338
339
        // We rebuild the interpreter every run so that we don't accidentally carry over any state
340
        // between runs, such as fuel remaining.
341
1.43k
        let mut interpreter = build_interpreter(&testcase);
342
1.43k
        let int_res = run_in_interpreter(&mut interpreter, args);
343
1.43k
        match int_res {
344
1.41k
            RunResult::Success(_) => {
345
1.41k
                STATISTICS.run_result_success.fetch_add(1, Ordering::SeqCst);
346
1.41k
            }
347
0
            RunResult::Trap(trap) => {
348
0
                STATISTICS.run_result_trap[&trap].fetch_add(1, Ordering::SeqCst);
349
                // If this input traps, skip it and continue trying other inputs
350
                // for this function. We've already compiled it anyway.
351
                //
352
                // We could catch traps in the host run and compare them to the
353
                // interpreter traps, but since we already test trap cases with
354
                // wasm tests and wasm-level fuzzing, the amount of effort does
355
                // not justify implementing it again here.
356
0
                continue;
357
            }
358
            RunResult::Timeout => {
359
                // We probably generated an infinite loop, we should drop this entire input.
360
                // We could `continue` like we do on traps, but timeouts are *really* expensive.
361
15
                STATISTICS.run_result_timeout.fetch_add(1, Ordering::SeqCst);
362
15
                return;
363
            }
364
0
            RunResult::Error(e) => panic!("interpreter failed: {e:?}"),
365
        }
366
367
1.41k
        let res = run(args);
368
369
        // This situation can happen when we are comparing the interpreter against the interpreter, and
370
        // one of the optimization passes has increased the number of instructions in the function.
371
        // This can cause the interpreter to run out of fuel in the second run, but not the first.
372
        // We should ignore these cases.
373
        // Running in the host should never return a timeout, so that should be ok.
374
1.41k
        if res == RunResult::Timeout {
375
0
            return;
376
1.41k
        }
377
378
1.41k
        assert_eq!(int_res, res);
379
    }
380
327
}
cranelift_fuzzgen::run_test_inputs::<cranelift_fuzzgen::_::__libfuzzer_sys_run::{closure#3}>
Line
Count
Source
335
6.34k
fn run_test_inputs(testcase: &TestCase, run: impl Fn(&[DataValue]) -> RunResult) {
336
18.5k
    for args in &testcase.inputs {
337
12.7k
        STATISTICS.total_runs.fetch_add(1, Ordering::SeqCst);
338
339
        // We rebuild the interpreter every run so that we don't accidentally carry over any state
340
        // between runs, such as fuel remaining.
341
12.7k
        let mut interpreter = build_interpreter(&testcase);
342
12.7k
        let int_res = run_in_interpreter(&mut interpreter, args);
343
12.7k
        match int_res {
344
12.0k
            RunResult::Success(_) => {
345
12.0k
                STATISTICS.run_result_success.fetch_add(1, Ordering::SeqCst);
346
12.0k
            }
347
181
            RunResult::Trap(trap) => {
348
181
                STATISTICS.run_result_trap[&trap].fetch_add(1, Ordering::SeqCst);
349
                // If this input traps, skip it and continue trying other inputs
350
                // for this function. We've already compiled it anyway.
351
                //
352
                // We could catch traps in the host run and compare them to the
353
                // interpreter traps, but since we already test trap cases with
354
                // wasm tests and wasm-level fuzzing, the amount of effort does
355
                // not justify implementing it again here.
356
181
                continue;
357
            }
358
            RunResult::Timeout => {
359
                // We probably generated an infinite loop, we should drop this entire input.
360
                // We could `continue` like we do on traps, but timeouts are *really* expensive.
361
465
                STATISTICS.run_result_timeout.fetch_add(1, Ordering::SeqCst);
362
465
                return;
363
            }
364
0
            RunResult::Error(e) => panic!("interpreter failed: {e:?}"),
365
        }
366
367
12.0k
        let res = run(args);
368
369
        // This situation can happen when we are comparing the interpreter against the interpreter, and
370
        // one of the optimization passes has increased the number of instructions in the function.
371
        // This can cause the interpreter to run out of fuel in the second run, but not the first.
372
        // We should ignore these cases.
373
        // Running in the host should never return a timeout, so that should be ok.
374
12.0k
        if res == RunResult::Timeout {
375
0
            return;
376
12.0k
        }
377
378
12.0k
        assert_eq!(int_res, res);
379
    }
380
6.34k
}
381
382
fuzz_target!(|testcase: TestCase| {
383
    let mut testcase = testcase;
384
    let fuel: u8 = std::env::args()
385
40.0k
        .find_map(|arg| arg.strip_prefix("--fuel=").map(|s| s.to_owned()))
386
0
        .map(|fuel| fuel.parse().expect("fuel should be a valid integer"))
387
        .unwrap_or_default();
388
    for i in 0..testcase.ctrl_planes.len() {
389
        testcase.ctrl_planes[i].set_fuel(fuel)
390
    }
391
    let testcase = testcase;
392
393
    // This is the default, but we should ensure that it wasn't accidentally turned off anywhere.
394
    assert!(testcase.isa.flags().enable_verifier());
395
396
    // Periodically print statistics
397
    let valid_inputs = STATISTICS.valid_inputs.fetch_add(1, Ordering::SeqCst);
398
    if valid_inputs != 0 && valid_inputs % 10000 == 0 {
399
        STATISTICS.print(valid_inputs);
400
    }
401
402
    if !testcase.compare_against_host {
403
        let opt_testcase = testcase.to_optimized();
404
405
1.41k
        run_test_inputs(&testcase, |args| {
406
            // We rebuild the interpreter every run so that we don't accidentally carry over any state
407
            // between runs, such as fuel remaining.
408
1.41k
            let mut interpreter = build_interpreter(&opt_testcase);
409
410
1.41k
            run_in_interpreter(&mut interpreter, args)
411
1.41k
        });
412
    } else {
413
        let mut compiler = TestFileCompiler::new(testcase.isa.clone());
414
        compiler
415
            .add_functions(&testcase.functions[..], testcase.ctrl_planes.clone())
416
            .unwrap();
417
        let compiled = compiler.compile().unwrap();
418
        let trampoline = compiled.get_trampoline(testcase.main()).unwrap();
419
420
12.0k
        run_test_inputs(&testcase, |args| run_in_host(&compiled, &trampoline, args));
421
    }
422
});