wasmtime_fuzzing/oracles/
stacks.rs

1use crate::generators::Stacks;
2use anyhow::bail;
3use 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.
9pub fn check_stacks(stacks: Stacks) -> usize {
10    let wasm = stacks.wasm();
11    crate::oracles::log_wasm(&wasm);
12
13    let engine = Engine::default();
14    let module = Module::new(&engine, &wasm).expect("should compile okay");
15
16    let mut linker = Linker::new(&engine);
17    linker
18        .func_wrap(
19            "host",
20            "check_stack",
21            |mut caller: Caller<'_, ()>| -> Result<()> {
22                let fuel = caller
23                    .get_export("fuel")
24                    .expect("should export `fuel`")
25                    .into_global()
26                    .expect("`fuel` export should be a global");
27
28                let fuel_left = fuel.get(&mut caller).unwrap_i32();
29                if fuel_left == 0 {
30                    bail!(Trap::OutOfFuel);
31                }
32
33                fuel.set(&mut caller, Val::I32(fuel_left - 1)).unwrap();
34                Ok(())
35            },
36        )
37        .unwrap()
38        .func_wrap(
39            "host",
40            "call_func",
41            |mut caller: Caller<'_, ()>, f: Option<Func>| {
42                let f = f.unwrap();
43                let ty = f.ty(&caller);
44                let params = vec![Val::I32(0); ty.params().len()];
45                let mut results = vec![Val::I32(0); ty.results().len()];
46                f.call(&mut caller, &params, &mut results)?;
47                Ok(())
48            },
49        )
50        .unwrap();
51
52    let mut store = Store::new(&engine, ());
53
54    let instance = linker
55        .instantiate(&mut store, &module)
56        .expect("should instantiate okay");
57
58    let run = instance
59        .get_typed_func::<(u32,), ()>(&mut store, "run")
60        .expect("should export `run` function");
61
62    let mut max_stack_depth = 0;
63    for input in stacks.inputs().iter().copied() {
64        log::debug!("input: {}", input);
65        if let Err(trap) = run.call(&mut store, (input.into(),)) {
66            log::debug!("trap: {:?}", trap);
67            let get_stack = instance
68                .get_typed_func::<(), (u32, u32)>(&mut store, "get_stack")
69                .expect("should export `get_stack` function as expected");
70
71            let (ptr, len) = get_stack
72                .call(&mut store, ())
73                .expect("`get_stack` should not trap");
74
75            let memory = instance
76                .get_memory(&mut store, "memory")
77                .expect("should have `memory` export");
78
79            let host_trace = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
80            let trap = trap.downcast_ref::<Trap>().unwrap();
81            max_stack_depth = max_stack_depth.max(host_trace.len());
82            assert_stack_matches(&mut store, memory, ptr, len, host_trace, *trap);
83        }
84    }
85    max_stack_depth
86}
87
88/// Assert that the Wasm program's view of the stack matches the host's view.
89fn assert_stack_matches(
90    store: &mut impl AsContextMut,
91    memory: Memory,
92    ptr: u32,
93    len: u32,
94    host_trace: &[FrameInfo],
95    trap: Trap,
96) {
97    let mut data = vec![0; len as usize];
98    memory
99        .read(&mut *store, ptr as usize, &mut data)
100        .expect("should be in bounds");
101
102    let mut wasm_trace = vec![];
103    for entry in data.chunks(4).rev() {
104        let mut bytes = [0; 4];
105        bytes.copy_from_slice(entry);
106        let entry = u32::from_le_bytes(bytes);
107        wasm_trace.push(entry);
108    }
109
110    // If the test case here trapped due to stack overflow then the host trace
111    // will have one more frame than the wasm trace. The wasm didn't actually
112    // get to the point of pushing onto its own trace stack where the host will
113    // be able to see the exact function that triggered the stack overflow. In
114    // this situation the host trace is asserted to be one larger and then the
115    // top frame (first) of the host trace is discarded.
116    let host_trace = if trap == Trap::StackOverflow {
117        assert_eq!(host_trace.len(), wasm_trace.len() + 1);
118        &host_trace[1..]
119    } else {
120        host_trace
121    };
122
123    log::debug!("Wasm thinks its stack is: {:?}", wasm_trace);
124    log::debug!(
125        "Host thinks the stack is: {:?}",
126        host_trace
127            .iter()
128            .map(|f| f.func_index())
129            .collect::<Vec<_>>()
130    );
131
132    assert_eq!(wasm_trace.len(), host_trace.len());
133    for (wasm_entry, host_entry) in wasm_trace.into_iter().zip(host_trace) {
134        assert_eq!(wasm_entry, host_entry.func_index());
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use arbitrary::{Arbitrary, Unstructured};
142    use rand::prelude::*;
143
144    const TARGET_STACK_DEPTH: usize = 10;
145
146    #[test]
147    fn smoke_test() {
148        let mut rng = SmallRng::seed_from_u64(0);
149        let mut buf = vec![0; 2048];
150
151        for _ in 0..1024 {
152            rng.fill_bytes(&mut buf);
153            let u = Unstructured::new(&buf);
154            if let Ok(stacks) = Stacks::arbitrary_take_rest(u) {
155                let max_stack_depth = check_stacks(stacks);
156                if max_stack_depth >= TARGET_STACK_DEPTH {
157                    return;
158                }
159            }
160        }
161
162        panic!(
163            "never generated a `Stacks` test case that reached {TARGET_STACK_DEPTH} \
164             deep stack frames",
165        );
166    }
167}