/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 = ¶ms[..]; |
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 | | } |