wasmtime_runtime/traphandlers/
backtrace.rs

1//! Backtrace and stack walking functionality for Wasm.
2//!
3//! Walking the Wasm stack is comprised of
4//!
5//! 1. identifying sequences of contiguous Wasm frames on the stack
6//!    (i.e. skipping over native host frames), and
7//!
8//! 2. walking the Wasm frames within such a sequence.
9//!
10//! To perform (1) we maintain the entry stack pointer (SP) and exit frame
11//! pointer (FP) and program counter (PC) each time we call into Wasm and Wasm
12//! calls into the host via trampolines (see
13//! `crates/runtime/src/trampolines`). The most recent entry is stored in
14//! `VMRuntimeLimits` and older entries are saved in `CallThreadState`. This
15//! lets us identify ranges of contiguous Wasm frames on the stack.
16//!
17//! To solve (2) and walk the Wasm frames within a region of contiguous Wasm
18//! frames on the stack, we configure Cranelift's `preserve_frame_pointers =
19//! true` setting. Then we can do simple frame pointer traversal starting at the
20//! exit FP and stopping once we reach the entry SP (meaning that the next older
21//! frame is a host frame).
22
23use crate::arch;
24use crate::{
25    traphandlers::{tls, CallThreadState},
26    VMRuntimeLimits,
27};
28use std::ops::ControlFlow;
29
30/// A WebAssembly stack trace.
31#[derive(Debug)]
32pub struct Backtrace(Vec<Frame>);
33
34/// A stack frame within a Wasm stack trace.
35#[derive(Debug)]
36pub struct Frame {
37    pc: usize,
38    fp: usize,
39}
40
41impl Frame {
42    /// Get this frame's program counter.
43    pub fn pc(&self) -> usize {
44        self.pc
45    }
46
47    /// Get this frame's frame pointer.
48    pub fn fp(&self) -> usize {
49        self.fp
50    }
51}
52
53impl Backtrace {
54    /// Returns an empty backtrace
55    pub fn empty() -> Backtrace {
56        Backtrace(Vec::new())
57    }
58
59    /// Capture the current Wasm stack in a backtrace.
60    pub fn new(limits: *const VMRuntimeLimits) -> Backtrace {
61        tls::with(|state| match state {
62            Some(state) => unsafe { Self::new_with_trap_state(limits, state, None) },
63            None => Backtrace(vec![]),
64        })
65    }
66
67    /// Capture the current Wasm stack trace.
68    ///
69    /// If Wasm hit a trap, and we calling this from the trap handler, then the
70    /// Wasm exit trampoline didn't run, and we use the provided PC and FP
71    /// instead of looking them up in `VMRuntimeLimits`.
72    pub(crate) unsafe fn new_with_trap_state(
73        limits: *const VMRuntimeLimits,
74        state: &CallThreadState,
75        trap_pc_and_fp: Option<(usize, usize)>,
76    ) -> Backtrace {
77        let mut frames = vec![];
78        Self::trace_with_trap_state(limits, state, trap_pc_and_fp, |frame| {
79            frames.push(frame);
80            ControlFlow::Continue(())
81        });
82        Backtrace(frames)
83    }
84
85    /// Walk the current Wasm stack, calling `f` for each frame we walk.
86    pub fn trace(limits: *const VMRuntimeLimits, f: impl FnMut(Frame) -> ControlFlow<()>) {
87        tls::with(|state| match state {
88            Some(state) => unsafe { Self::trace_with_trap_state(limits, state, None, f) },
89            None => {}
90        });
91    }
92
93    /// Walk the current Wasm stack, calling `f` for each frame we walk.
94    ///
95    /// If Wasm hit a trap, and we calling this from the trap handler, then the
96    /// Wasm exit trampoline didn't run, and we use the provided PC and FP
97    /// instead of looking them up in `VMRuntimeLimits`.
98    pub(crate) unsafe fn trace_with_trap_state(
99        limits: *const VMRuntimeLimits,
100        state: &CallThreadState,
101        trap_pc_and_fp: Option<(usize, usize)>,
102        mut f: impl FnMut(Frame) -> ControlFlow<()>,
103    ) {
104        log::trace!("====== Capturing Backtrace ======");
105
106        let (last_wasm_exit_pc, last_wasm_exit_fp) = match trap_pc_and_fp {
107            // If we exited Wasm by catching a trap, then the Wasm-to-host
108            // trampoline did not get a chance to save the last Wasm PC and FP,
109            // and we need to use the plumbed-through values instead.
110            Some((pc, fp)) => {
111                assert!(std::ptr::eq(limits, state.limits));
112                (pc, fp)
113            }
114            // Either there is no Wasm currently on the stack, or we exited Wasm
115            // through the Wasm-to-host trampoline.
116            None => {
117                let pc = *(*limits).last_wasm_exit_pc.get();
118                let fp = *(*limits).last_wasm_exit_fp.get();
119                (pc, fp)
120            }
121        };
122
123        let activations = std::iter::once((
124            last_wasm_exit_pc,
125            last_wasm_exit_fp,
126            *(*limits).last_wasm_entry_sp.get(),
127        ))
128        .chain(
129            state
130                .iter()
131                .filter(|state| std::ptr::eq(limits, state.limits))
132                .map(|state| {
133                    (
134                        state.old_last_wasm_exit_pc(),
135                        state.old_last_wasm_exit_fp(),
136                        state.old_last_wasm_entry_sp(),
137                    )
138                }),
139        )
140        .take_while(|&(pc, fp, sp)| {
141            if pc == 0 {
142                debug_assert_eq!(fp, 0);
143                debug_assert_eq!(sp, 0);
144            }
145            pc != 0
146        });
147
148        for (pc, fp, sp) in activations {
149            if let ControlFlow::Break(()) = Self::trace_through_wasm(pc, fp, sp, &mut f) {
150                log::trace!("====== Done Capturing Backtrace (closure break) ======");
151                return;
152            }
153        }
154
155        log::trace!("====== Done Capturing Backtrace (reached end of activations) ======");
156    }
157
158    /// Walk through a contiguous sequence of Wasm frames starting with the
159    /// frame at the given PC and FP and ending at `trampoline_sp`.
160    unsafe fn trace_through_wasm(
161        mut pc: usize,
162        mut fp: usize,
163        trampoline_sp: usize,
164        mut f: impl FnMut(Frame) -> ControlFlow<()>,
165    ) -> ControlFlow<()> {
166        log::trace!("=== Tracing through contiguous sequence of Wasm frames ===");
167        log::trace!("trampoline_sp = 0x{:016x}", trampoline_sp);
168        log::trace!("   initial pc = 0x{:016x}", pc);
169        log::trace!("   initial fp = 0x{:016x}", fp);
170
171        // We already checked for this case in the `trace_with_trap_state`
172        // caller.
173        assert_ne!(pc, 0);
174        assert_ne!(fp, 0);
175        assert_ne!(trampoline_sp, 0);
176
177        arch::assert_entry_sp_is_aligned(trampoline_sp);
178
179        loop {
180            // At the start of each iteration of the loop, we know that `fp` is
181            // a frame pointer from Wasm code. Therefore, we know it is not
182            // being used as an extra general-purpose register, and it is safe
183            // dereference to get the PC and the next older frame pointer.
184
185            // The stack grows down, and therefore any frame pointer we are
186            // dealing with should be less than the stack pointer on entry
187            // to Wasm.
188            assert!(trampoline_sp >= fp, "{trampoline_sp:#x} >= {fp:#x}");
189
190            arch::assert_fp_is_aligned(fp);
191
192            log::trace!("--- Tracing through one Wasm frame ---");
193            log::trace!("pc = {:p}", pc as *const ());
194            log::trace!("fp = {:p}", fp as *const ());
195
196            f(Frame { pc, fp })?;
197
198            pc = arch::get_next_older_pc_from_fp(fp);
199
200            // We rely on this offset being zero for all supported architectures
201            // in `crates/cranelift/src/component/compiler.rs` when we set the
202            // Wasm exit FP. If this ever changes, we will need to update that
203            // code as well!
204            assert_eq!(arch::NEXT_OLDER_FP_FROM_FP_OFFSET, 0);
205
206            // Get the next older frame pointer from the current Wasm frame
207            // pointer.
208            //
209            // The next older frame pointer may or may not be a Wasm frame's
210            // frame pointer, but it is trusted either way (i.e. is actually a
211            // frame pointer and not being used as a general-purpose register)
212            // because we always enter Wasm from the host via a trampoline, and
213            // this trampoline maintains a proper frame pointer.
214            //
215            // We want to detect when we've reached the trampoline, and break
216            // out of this stack-walking loop. All of our architectures' stacks
217            // grow down and look something vaguely like this:
218            //
219            //     | ...               |
220            //     | Native Frames     |
221            //     | ...               |
222            //     |-------------------|
223            //     | ...               | <-- Trampoline FP            |
224            //     | Trampoline Frame  |                              |
225            //     | ...               | <-- Trampoline SP            |
226            //     |-------------------|                            Stack
227            //     | Return Address    |                            Grows
228            //     | Previous FP       | <-- Wasm FP                Down
229            //     | ...               |                              |
230            //     | Wasm Frames       |                              |
231            //     | ...               |                              V
232            //
233            // The trampoline records its own stack pointer (`trampoline_sp`),
234            // which is guaranteed to be above all Wasm frame pointers but at or
235            // below its own frame pointer. It is usually two words above the
236            // Wasm frame pointer (at least on x86-64, exact details vary across
237            // architectures) but not always: if the first Wasm function called
238            // by the host has many arguments, some of them could be passed on
239            // the stack in between the return address and the trampoline's
240            // frame.
241            //
242            // To check when we've reached the trampoline frame, it is therefore
243            // sufficient to check when the next frame pointer is greater than
244            // or equal to `trampoline_sp` (except s390x, where it needs to be
245            // strictly greater than).
246            let next_older_fp = *(fp as *mut usize).add(arch::NEXT_OLDER_FP_FROM_FP_OFFSET);
247            if arch::reached_entry_sp(next_older_fp, trampoline_sp) {
248                log::trace!("=== Done tracing contiguous sequence of Wasm frames ===");
249                return ControlFlow::Continue(());
250            }
251
252            // Because the stack always grows down, the older FP must be greater
253            // than the current FP.
254            assert!(next_older_fp > fp, "{next_older_fp:#x} > {fp:#x}");
255            fp = next_older_fp;
256        }
257    }
258
259    /// Iterate over the frames inside this backtrace.
260    pub fn frames<'a>(
261        &'a self,
262    ) -> impl ExactSizeIterator<Item = &'a Frame> + DoubleEndedIterator + 'a {
263        self.0.iter()
264    }
265}