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.
check_stacks(stacks: Stacks) -> usize9 pub fn check_stacks(stacks: Stacks) -> usize {
10 let wasm = stacks.wasm();
11 crate::oracles::log_wasm(&wasm);
12
13 let mut config = Config::new();
14 config.wasm_backtrace_max_frames(stacks.limit);
15 let engine = Engine::new(&config).unwrap();
16 let module = Module::new(&engine, &wasm).expect("should compile okay");
17
18 let mut linker = Linker::new(&engine);
19 linker
20 .func_wrap(
21 "host",
22 "check_stack",
23 |mut caller: Caller<'_, ()>| -> Result<()> {
24 let fuel = caller
25 .get_export("fuel")
26 .expect("should export `fuel`")
27 .into_global()
28 .expect("`fuel` export should be a global");
29
30 let fuel_left = fuel.get(&mut caller).unwrap_i32();
31 if fuel_left == 0 {
32 bail!(Trap::OutOfFuel);
33 }
34
35 fuel.set(&mut caller, Val::I32(fuel_left - 1)).unwrap();
36 Ok(())
37 },
38 )
39 .unwrap()
40 .func_wrap(
41 "host",
42 "call_func",
43 |mut caller: Caller<'_, ()>, f: Option<Func>| {
44 let f = f.unwrap();
45 let ty = f.ty(&caller);
46 let params = vec![Val::I32(0); ty.params().len()];
47 let mut results = vec![Val::I32(0); ty.results().len()];
48 f.call(&mut caller, ¶ms, &mut results)?;
49 Ok(())
50 },
51 )
52 .unwrap();
53
54 let mut store = Store::new(&engine, ());
55
56 let instance = linker
57 .instantiate(&mut store, &module)
58 .expect("should instantiate okay");
59
60 let run = instance
61 .get_typed_func::<(u32,), ()>(&mut store, "run")
62 .expect("should export `run` function");
63
64 let mut max_stack_depth = 0;
65 for input in stacks.inputs().iter().copied() {
66 log::debug!("input: {input}");
67 if let Err(trap) = run.call(&mut store, (input.into(),)) {
68 log::debug!("trap: {trap:?}");
69 let get_stack = instance
70 .get_typed_func::<(), (u32, u32)>(&mut store, "get_stack")
71 .expect("should export `get_stack` function as expected");
72
73 let (ptr, len) = get_stack
74 .call(&mut store, ())
75 .expect("`get_stack` should not trap");
76
77 let memory = instance
78 .get_memory(&mut store, "memory")
79 .expect("should have `memory` export");
80
81 let host_trace = match trap.downcast_ref::<WasmBacktrace>() {
82 Some(bt) => bt.frames(),
83 None => {
84 assert!(stacks.limit.is_none());
85 continue;
86 }
87 };
88 let trap = trap.downcast_ref::<Trap>().unwrap();
89 max_stack_depth = max_stack_depth.max(host_trace.len());
90 assert_stack_matches(
91 &mut store,
92 memory,
93 ptr,
94 len,
95 host_trace,
96 *trap,
97 stacks.limit,
98 );
99 }
100 }
101 max_stack_depth
102 }
103
104 /// Assert that the Wasm program's view of the stack matches the host's view.
assert_stack_matches( store: &mut impl AsContextMut, memory: Memory, ptr: u32, len: u32, host_trace: &[FrameInfo], trap: Trap, limit: Option<std::num::NonZeroUsize>, )105 fn assert_stack_matches(
106 store: &mut impl AsContextMut,
107 memory: Memory,
108 ptr: u32,
109 len: u32,
110 host_trace: &[FrameInfo],
111 trap: Trap,
112 limit: Option<std::num::NonZeroUsize>,
113 ) {
114 let mut data = vec![0; len as usize];
115 memory
116 .read(&mut *store, ptr as usize, &mut data)
117 .expect("should be in bounds");
118
119 let mut wasm_trace = vec![];
120 for entry in data.chunks(4).rev() {
121 let mut bytes = [0; 4];
122 bytes.copy_from_slice(entry);
123 let entry = u32::from_le_bytes(bytes);
124 wasm_trace.push(entry);
125 }
126
127 let trace_limit = match limit {
128 Some(n) => n.get(),
129 None => {
130 // Backtraces are disabled; the host trace should be empty.
131 assert!(host_trace.is_empty());
132 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 let (host_trace, wasm_trace) = if trap == Trap::StackOverflow {
143 if host_trace.len() == trace_limit {
144 assert!(
145 trace_limit <= wasm_trace.len() + 1,
146 "Host trace size {} is larger than expected {}",
147 trace_limit,
148 wasm_trace.len() + 1
149 );
150 } else {
151 assert_eq!(host_trace.len(), wasm_trace.len() + 1);
152 }
153 (
154 &host_trace[1..],
155 &wasm_trace.get(..trace_limit - 1).unwrap_or(&wasm_trace),
156 )
157 } else {
158 (
159 host_trace,
160 &wasm_trace.get(..trace_limit).unwrap_or(&wasm_trace),
161 )
162 };
163
164 log::debug!("Wasm thinks its stack is: {wasm_trace:?}");
165 log::debug!(
166 "Host thinks the stack is: {:?}",
167 host_trace
168 .iter()
169 .map(|f| f.func_index())
170 .collect::<Vec<_>>()
171 );
172
173 assert_eq!(wasm_trace.len(), host_trace.len());
174 for (wasm_entry, host_entry) in wasm_trace.into_iter().zip(host_trace) {
175 assert_eq!(wasm_entry, &host_entry.func_index());
176 }
177 }
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]
smoke_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 }
194