Coverage Report

Created: 2026-09-28 08:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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, &params, &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
}