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}