Coverage Report

Created: 2026-02-14 06:58

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