/src/wasmtime/crates/fuzzing/src/oracles/stacks.rs
Line | Count | Source |
1 | | use crate::generators::Stacks; |
2 | | use wasmtime::bail; |
3 | | use wasmtime::*; |
4 | | |
5 | | /// Run the given `Stacks` test case and assert that the host's view of the Wasm |
6 | | /// stack matches the test case's understanding of the Wasm stack. |
7 | | /// |
8 | | /// Returns the maximum stack depth we checked. |
9 | 2.11k | pub fn check_stacks(stacks: Stacks) -> usize { |
10 | 2.11k | let wasm = stacks.wasm(); |
11 | 2.11k | crate::oracles::log_wasm(&wasm); |
12 | | |
13 | 2.11k | let mut config = Config::new(); |
14 | 2.11k | config.wasm_backtrace_max_frames(stacks.limit); |
15 | 2.11k | let engine = Engine::new(&config).unwrap(); |
16 | 2.11k | let module = Module::new(&engine, &wasm).expect("should compile okay"); |
17 | | |
18 | 2.11k | let mut linker = Linker::new(&engine); |
19 | 2.11k | linker |
20 | 2.11k | .func_wrap( |
21 | 2.11k | "host", |
22 | 2.11k | "check_stack", |
23 | 1.08M | |mut caller: Caller<'_, ()>| -> Result<()> { |
24 | 1.08M | let fuel = caller |
25 | 1.08M | .get_export("fuel") |
26 | 1.08M | .expect("should export `fuel`") |
27 | 1.08M | .into_global() |
28 | 1.08M | .expect("`fuel` export should be a global"); |
29 | | |
30 | 1.08M | let fuel_left = fuel.get(&mut caller).unwrap_i32(); |
31 | 1.08M | if fuel_left == 0 { |
32 | 10.4k | bail!(Trap::OutOfFuel); |
33 | 1.07M | } |
34 | | |
35 | 1.07M | fuel.set(&mut caller, Val::I32(fuel_left - 1)).unwrap(); |
36 | 1.07M | Ok(()) |
37 | 1.08M | }, |
38 | | ) |
39 | 2.11k | .unwrap() |
40 | 2.11k | .func_wrap( |
41 | 2.11k | "host", |
42 | 2.11k | "call_func", |
43 | 884k | |mut caller: Caller<'_, ()>, f: Option<Func>| { |
44 | 884k | let f = f.unwrap(); |
45 | 884k | let ty = f.ty(&caller); |
46 | 884k | let params = vec![Val::I32(0); ty.params().len()]; |
47 | 884k | let mut results = vec![Val::I32(0); ty.results().len()]; |
48 | 884k | f.call(&mut caller, ¶ms, &mut results)?; |
49 | 39.7k | Ok(()) |
50 | 884k | }, |
51 | | ) |
52 | 2.11k | .unwrap(); |
53 | | |
54 | 2.11k | let mut store = Store::new(&engine, ()); |
55 | | |
56 | 2.11k | let instance = linker |
57 | 2.11k | .instantiate(&mut store, &module) |
58 | 2.11k | .expect("should instantiate okay"); |
59 | | |
60 | 2.11k | let run = instance |
61 | 2.11k | .get_typed_func::<(u32,), ()>(&mut store, "run") |
62 | 2.11k | .expect("should export `run` function"); |
63 | | |
64 | 2.11k | let mut max_stack_depth = 0; |
65 | 98.2k | for input in stacks.inputs().iter().copied() { |
66 | 98.2k | log::debug!("input: {input}"); |
67 | 98.2k | if let Err(trap) = run.call(&mut store, (input.into(),)) { |
68 | 82.9k | log::debug!("trap: {trap:?}"); |
69 | 82.9k | let get_stack = instance |
70 | 82.9k | .get_typed_func::<(), (u32, u32)>(&mut store, "get_stack") |
71 | 82.9k | .expect("should export `get_stack` function as expected"); |
72 | | |
73 | 82.9k | let (ptr, len) = get_stack |
74 | 82.9k | .call(&mut store, ()) |
75 | 82.9k | .expect("`get_stack` should not trap"); |
76 | | |
77 | 82.9k | let memory = instance |
78 | 82.9k | .get_memory(&mut store, "memory") |
79 | 82.9k | .expect("should have `memory` export"); |
80 | | |
81 | 82.9k | let host_trace = match trap.downcast_ref::<WasmBacktrace>() { |
82 | 35.4k | Some(bt) => bt.frames(), |
83 | | None => { |
84 | 47.4k | assert!(stacks.limit.is_none()); |
85 | 47.4k | continue; |
86 | | } |
87 | | }; |
88 | 35.4k | let trap = trap.downcast_ref::<Trap>().unwrap(); |
89 | 35.4k | max_stack_depth = max_stack_depth.max(host_trace.len()); |
90 | 35.4k | assert_stack_matches( |
91 | 35.4k | &mut store, |
92 | 35.4k | memory, |
93 | 35.4k | ptr, |
94 | 35.4k | len, |
95 | 35.4k | host_trace, |
96 | 35.4k | *trap, |
97 | 35.4k | stacks.limit, |
98 | | ); |
99 | 15.3k | } |
100 | | } |
101 | 2.11k | max_stack_depth |
102 | 2.11k | } |
103 | | |
104 | | /// Assert that the Wasm program's view of the stack matches the host's view. |
105 | 35.4k | fn assert_stack_matches( |
106 | 35.4k | store: &mut impl AsContextMut, |
107 | 35.4k | memory: Memory, |
108 | 35.4k | ptr: u32, |
109 | 35.4k | len: u32, |
110 | 35.4k | host_trace: &[FrameInfo], |
111 | 35.4k | trap: Trap, |
112 | 35.4k | limit: Option<std::num::NonZeroUsize>, |
113 | 35.4k | ) { |
114 | 35.4k | let mut data = vec![0; len as usize]; |
115 | 35.4k | memory |
116 | 35.4k | .read(&mut *store, ptr as usize, &mut data) |
117 | 35.4k | .expect("should be in bounds"); |
118 | | |
119 | 35.4k | let mut wasm_trace = vec![]; |
120 | 1.20M | for entry in data.chunks(4).rev() { |
121 | 1.20M | let mut bytes = [0; 4]; |
122 | 1.20M | bytes.copy_from_slice(entry); |
123 | 1.20M | let entry = u32::from_le_bytes(bytes); |
124 | 1.20M | wasm_trace.push(entry); |
125 | 1.20M | } |
126 | | |
127 | 35.4k | let trace_limit = match limit { |
128 | 35.4k | Some(n) => n.get(), |
129 | | None => { |
130 | | // Backtraces are disabled; the host trace should be empty. |
131 | 0 | assert!(host_trace.is_empty()); |
132 | 0 | return; |
133 | | } |
134 | | }; |
135 | | |
136 | | // If the test case here trapped due to stack overflow then the host trace |
137 | | // will have one more frame than the wasm trace. The wasm didn't actually |
138 | | // get to the point of pushing onto its own trace stack where the host will |
139 | | // be able to see the exact function that triggered the stack overflow. In |
140 | | // this situation the host trace is asserted to be one larger and then the |
141 | | // top frame (first) of the host trace is discarded. |
142 | 35.4k | let (host_trace, wasm_trace) = if trap == Trap::StackOverflow { |
143 | 1.67k | if host_trace.len() == trace_limit { |
144 | 548 | assert!( |
145 | 548 | trace_limit <= wasm_trace.len() + 1, |
146 | | "Host trace size {} is larger than expected {}", |
147 | | trace_limit, |
148 | 0 | wasm_trace.len() + 1 |
149 | | ); |
150 | | } else { |
151 | 1.12k | assert_eq!(host_trace.len(), wasm_trace.len() + 1); |
152 | | } |
153 | 1.67k | ( |
154 | 1.67k | &host_trace[1..], |
155 | 1.67k | &wasm_trace.get(..trace_limit - 1).unwrap_or(&wasm_trace), |
156 | 1.67k | ) |
157 | | } else { |
158 | 33.7k | ( |
159 | 33.7k | host_trace, |
160 | 33.7k | &wasm_trace.get(..trace_limit).unwrap_or(&wasm_trace), |
161 | 33.7k | ) |
162 | | }; |
163 | | |
164 | 35.4k | log::debug!("Wasm thinks its stack is: {wasm_trace:?}"); |
165 | 35.4k | log::debug!( |
166 | | "Host thinks the stack is: {:?}", |
167 | 0 | host_trace |
168 | 0 | .iter() |
169 | 0 | .map(|f| f.func_index()) |
170 | 0 | .collect::<Vec<_>>() |
171 | | ); |
172 | | |
173 | 35.4k | assert_eq!(wasm_trace.len(), host_trace.len()); |
174 | 815k | for (wasm_entry, host_entry) in wasm_trace.into_iter().zip(host_trace) { |
175 | 815k | assert_eq!(wasm_entry, &host_entry.func_index()); |
176 | | } |
177 | 35.4k | } |
178 | | |
179 | | #[cfg(test)] |
180 | | mod tests { |
181 | | use super::*; |
182 | | use crate::test::gen_until_pass; |
183 | | |
184 | | const TARGET_STACK_DEPTH: usize = 10; |
185 | | |
186 | | #[test] |
187 | | fn smoke_test() { |
188 | | gen_until_pass(|stacks: Stacks, _u| { |
189 | | let max_stack_depth = check_stacks(stacks); |
190 | | Ok(max_stack_depth >= TARGET_STACK_DEPTH) |
191 | | }); |
192 | | } |
193 | | } |