Coverage Report

Created: 2025-10-10 07:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wasmi/fuzz/fuzz_targets/differential.rs
Line
Count
Source
1
#![no_main]
2
3
use arbitrary::{Arbitrary, Unstructured};
4
use libfuzzer_sys::fuzz_target;
5
use wasmi::ValType;
6
use wasmi_fuzz::{
7
    config::FuzzSmithConfig,
8
    oracle::{
9
        ChosenOracle,
10
        DifferentialOracle,
11
        DifferentialOracleMeta,
12
        ModuleExports,
13
        WasmiOracle,
14
    },
15
    FuzzError,
16
    FuzzModule,
17
    FuzzVal,
18
    FuzzValType,
19
};
20
21
/// Fuzzing input for differential fuzzing.
22
#[derive(Debug)]
23
pub struct FuzzInput<'a> {
24
    /// The chosen Wasm runtime oracle to compare against Wasmi.
25
    chosen_oracle: ChosenOracle,
26
    /// The fuzzed Wasm module and its configuration.
27
    module: FuzzModule,
28
    /// Additional unstructured input data used to initialize call parameter etc.
29
    u: Unstructured<'a>,
30
}
31
32
impl<'a> Arbitrary<'a> for FuzzInput<'a> {
33
18.8k
    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
34
18.8k
        let mut fuzz_config = FuzzSmithConfig::arbitrary(u)?;
35
18.8k
        fuzz_config.enable_nan_canonicalization();
36
18.8k
        fuzz_config.export_everything();
37
18.8k
        let chosen_oracle = ChosenOracle::arbitrary(u).unwrap_or_default();
38
18.8k
        WasmiOracle::configure(&mut fuzz_config);
39
18.8k
        chosen_oracle.configure(&mut fuzz_config);
40
18.8k
        let smith_config: wasm_smith::Config = fuzz_config.into();
41
18.8k
        let mut smith_module = FuzzModule::new(smith_config, u)?;
42
18.8k
        smith_module.ensure_termination(1_000 /* fuel */);
43
18.8k
        Ok(Self {
44
18.8k
            chosen_oracle,
45
18.8k
            module: smith_module,
46
18.8k
            u: Unstructured::new(&[]),
47
18.8k
        })
48
18.8k
    }
49
50
18.8k
    fn arbitrary_take_rest(mut u: Unstructured<'a>) -> arbitrary::Result<Self> {
51
18.8k
        Self::arbitrary(&mut u).map(|mut input| {
52
18.8k
            input.u = u;
53
18.8k
            input
54
18.8k
        })
55
18.8k
    }
56
}
57
58
fuzz_target!(|input: FuzzInput| {
59
    let Some(mut state) = FuzzState::setup(input) else {
60
        return;
61
    };
62
    state.run()
63
});
64
65
/// The state required to drive the differential fuzzer for a single run.
66
pub struct FuzzState<'a> {
67
    wasmi_oracle: WasmiOracle,
68
    chosen_oracle: Box<dyn DifferentialOracle>,
69
    wasm: Box<[u8]>,
70
    u: Unstructured<'a>,
71
}
72
73
impl<'a> FuzzState<'a> {
74
    /// Sets up the oracles for the differential fuzzing if possible.
75
18.8k
    pub fn setup(input: FuzzInput<'a>) -> Option<Self> {
76
18.8k
        let wasm = input.module.wasm().into_bytes();
77
18.8k
        let wasmi_oracle = WasmiOracle::setup(&wasm[..])?;
78
16.0k
        let chosen_oracle = input.chosen_oracle.setup(&wasm[..])?;
79
15.8k
        Some(Self {
80
15.8k
            wasm,
81
15.8k
            wasmi_oracle,
82
15.8k
            chosen_oracle,
83
15.8k
            u: input.u,
84
15.8k
        })
85
18.8k
    }
86
87
    /// Performs the differential fuzzing.
88
15.8k
    pub fn run(&mut self) {
89
15.8k
        let exports = self.wasmi_oracle.exports();
90
15.8k
        let mut params = Vec::new();
91
269k
        for (name, func_type) in exports.funcs() {
92
269k
            self.init_params(&mut params, func_type.params());
93
269k
            let params = &params[..];
94
269k
            let result_wasmi = self.wasmi_oracle.call(name, params);
95
269k
            let result_oracle = self.chosen_oracle.call(name, params);
96
            // Note: If either of the oracles returns a non-deterministic error we skip the
97
            //       entire fuzz run since following function executions could be affected by
98
            //       this non-determinism due to shared global state, such as global variables.
99
269k
            if let Err(wasmi_err) = &result_wasmi {
100
161k
                if wasmi_err.is_non_deterministic() {
101
2
                    return;
102
161k
                }
103
108k
            }
104
269k
            if let Err(oracle_err) = &result_oracle {
105
161k
                if oracle_err.is_non_deterministic() {
106
1
                    return;
107
161k
                }
108
108k
            }
109
269k
            match (result_wasmi, result_oracle) {
110
108k
                (Ok(wasmi_results), Ok(oracle_results)) => {
111
108k
                    self.assert_results_match(name, params, &wasmi_results, &oracle_results);
112
108k
                    self.assert_globals_match(&exports);
113
108k
                    self.assert_memories_match(&exports);
114
108k
                }
115
161k
                (Err(wasmi_err), Err(oracle_err)) => {
116
161k
                    self.assert_errors_match(name, params, wasmi_err, oracle_err);
117
161k
                    self.assert_globals_match(&exports);
118
161k
                    self.assert_memories_match(&exports);
119
161k
                }
120
0
                (wasmi_results, oracle_results) => {
121
0
                    self.report_divergent_behavior(name, params, &wasmi_results, &oracle_results)
122
                }
123
            }
124
        }
125
15.8k
    }
126
127
    /// Fill [`FuzzVal`]s of type `src` into `dst` using `u` for initialization.
128
    ///
129
    /// Clears `dst` before the operation.
130
269k
    fn init_params(&mut self, dst: &mut Vec<FuzzVal>, src: &[ValType]) {
131
269k
        dst.clear();
132
269k
        dst.extend(
133
269k
            src.iter()
134
269k
                .copied()
135
269k
                .map(FuzzValType::from)
136
1.00M
                .map(|ty| FuzzVal::with_type(ty, &mut self.u)),
137
        );
138
269k
    }
139
140
    /// Asserts that the call results is equal for both oracles.
141
108k
    fn assert_results_match(
142
108k
        &self,
143
108k
        func_name: &str,
144
108k
        params: &[FuzzVal],
145
108k
        wasmi_results: &[FuzzVal],
146
108k
        oracle_results: &[FuzzVal],
147
108k
    ) {
148
108k
        if wasmi_results == oracle_results {
149
108k
            return;
150
0
        }
151
0
        let wasmi_name = self.wasmi_oracle.name();
152
0
        let oracle_name = self.chosen_oracle.name();
153
0
        let crash_input = self.generate_crash_inputs();
154
0
        panic!(
155
0
            "\
156
0
            function call returned different values:\n\
157
0
                \tfunc: {func_name}\n\
158
0
                \tparams: {params:?}\n\
159
0
                \t{wasmi_name}: {wasmi_results:?}\n\
160
0
                \t{oracle_name}: {oracle_results:?}\n\
161
0
                \tcrash-report: 0x{crash_input}\n\
162
0
            "
163
        )
164
108k
    }
165
166
    /// Asserts that the call results is equal for both oracles.
167
161k
    fn assert_errors_match(
168
161k
        &self,
169
161k
        func_name: &str,
170
161k
        params: &[FuzzVal],
171
161k
        wasmi_err: FuzzError,
172
161k
        oracle_err: FuzzError,
173
161k
    ) {
174
161k
        if wasmi_err == oracle_err {
175
161k
            return;
176
0
        }
177
0
        let wasmi_name = self.wasmi_oracle.name();
178
0
        let oracle_name = self.chosen_oracle.name();
179
0
        let crash_input = self.generate_crash_inputs();
180
0
        panic!(
181
0
            "\
182
0
            function call returned different errors:\n\
183
0
                \tfunc: {func_name}\n\
184
0
                \tparams: {params:?}\n\
185
0
                \t{wasmi_name}: {wasmi_err:?}\n\
186
0
                \t{oracle_name}: {oracle_err:?}\n\
187
0
                \tcrash-report: 0x{crash_input}\n\
188
0
            "
189
        )
190
161k
    }
191
192
    /// Asserts that the global variable state is equal in both oracles.
193
269k
    fn assert_globals_match(&mut self, exports: &ModuleExports) {
194
860k
        for name in exports.globals() {
195
860k
            let wasmi_val = self.wasmi_oracle.get_global(name);
196
860k
            let oracle_val = self.chosen_oracle.get_global(name);
197
860k
            if wasmi_val == oracle_val {
198
860k
                continue;
199
0
            }
200
0
            let wasmi_name = self.wasmi_oracle.name();
201
0
            let oracle_name = self.chosen_oracle.name();
202
0
            let crash_input = self.generate_crash_inputs();
203
0
            panic!(
204
0
                "\
205
0
                encountered unequal globals:\n\
206
0
                    \tglobal: {name}\n\
207
0
                    \t{wasmi_name}: {wasmi_val:?}\n\
208
0
                    \t{oracle_name}: {oracle_val:?}\n\
209
0
                    \tcrash-report: 0x{crash_input}\n\
210
0
                "
211
            )
212
        }
213
269k
    }
214
215
    /// Asserts that the linear memory state is equal in both oracles.
216
269k
    fn assert_memories_match(&mut self, exports: &ModuleExports) {
217
304k
        for name in exports.memories() {
218
304k
            let Some(wasmi_mem) = self.wasmi_oracle.get_memory(name) else {
219
0
                continue;
220
            };
221
304k
            let Some(oracle_mem) = self.chosen_oracle.get_memory(name) else {
222
0
                continue;
223
            };
224
304k
            if wasmi_mem == oracle_mem {
225
304k
                continue;
226
0
            }
227
0
            let mut first_nonmatching = 0;
228
0
            let mut byte_wasmi = 0;
229
0
            let mut byte_oracle = 0;
230
0
            for (n, (mem0, mem1)) in wasmi_mem.iter().zip(oracle_mem).enumerate() {
231
0
                if mem0 != mem1 {
232
0
                    first_nonmatching = n;
233
0
                    byte_wasmi = *mem0;
234
0
                    byte_oracle = *mem1;
235
0
                    break;
236
0
                }
237
            }
238
0
            let wasmi_name = self.wasmi_oracle.name();
239
0
            let oracle_name = self.chosen_oracle.name();
240
0
            let crash_input = self.generate_crash_inputs();
241
0
            panic!(
242
0
                "\
243
0
                encountered unequal memories:\n\
244
0
                    \tmemory: {name}\n\
245
0
                    \tindex first non-matching: {first_nonmatching}\n\
246
0
                    \t{wasmi_name}: {byte_wasmi:?}\n\
247
0
                    \t{oracle_name}: {byte_oracle:?}\n\
248
0
                    \tcrash-report: 0x{crash_input}\n\
249
0
                "
250
            )
251
        }
252
269k
    }
253
254
    /// Reports divergent behavior between Wasmi and the chosen oracle.
255
0
    fn report_divergent_behavior(
256
0
        &self,
257
0
        func_name: &str,
258
0
        params: &[FuzzVal],
259
0
        wasmi_result: &Result<Box<[FuzzVal]>, FuzzError>,
260
0
        oracle_result: &Result<Box<[FuzzVal]>, FuzzError>,
261
0
    ) {
262
0
        assert!(matches!(
263
0
            (&wasmi_result, &oracle_result),
264
            (Ok(_), Err(_)) | (Err(_), Ok(_))
265
        ));
266
0
        let wasmi_name = self.wasmi_oracle.name();
267
0
        let oracle_name = self.chosen_oracle.name();
268
0
        let wasmi_state = match wasmi_result {
269
0
            Ok(_) => "returns result",
270
0
            Err(_) => "traps",
271
        };
272
0
        let oracle_state = match oracle_result {
273
0
            Ok(_) => "returns result",
274
0
            Err(_) => "traps",
275
        };
276
0
        let crash_input = self.generate_crash_inputs();
277
0
        panic!(
278
0
            "\
279
0
            function call {wasmi_state} for {wasmi_name} and {oracle_state} for {oracle_name}:\n\
280
0
                \tfunc: {func_name}\n\
281
0
                \tparams: {params:?}\n\
282
0
                \t{wasmi_name}: {wasmi_result:?}\n\
283
0
                \t{oracle_name}: {oracle_result:?}\n\
284
0
                \tcrash-report: 0x{crash_input}\n\
285
0
            "
286
        )
287
    }
288
289
    /// Generate crash input reports for `differential` fuzzing.`
290
    #[track_caller]
291
0
    fn generate_crash_inputs(&self) -> String {
292
0
        wasmi_fuzz::generate_crash_inputs("differential", &self.wasm).unwrap()
293
0
    }
294
}