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::traphandlers::{tls, CallThreadState};
24use cfg_if::cfg_if;
25use std::ops::ControlFlow;
26
27// Architecture-specific bits for stack walking. Each of these modules should
28// define and export the following functions:
29//
30// * `unsafe fn get_next_older_pc_from_fp(fp: usize) -> usize`
31// * `unsafe fn get_next_older_fp_from_fp(fp: usize) -> usize`
32// * `fn reached_entry_sp(fp: usize, first_wasm_sp: usize) -> bool`
33// * `fn assert_entry_sp_is_aligned(sp: usize)`
34// * `fn assert_fp_is_aligned(fp: usize)`
35cfg_if! {
36 if #[cfg(target_arch = "x86_64")] {
37 mod x86_64;
38 use x86_64 as arch;
39 } else if #[cfg(target_arch = "aarch64")] {
40 mod aarch64;
41 use aarch64 as arch;
42 } else if #[cfg(target_arch = "s390x")] {
43 mod s390x;
44 use s390x as arch;
45 } else if #[cfg(target_arch = "riscv64")] {
46 mod riscv64;
47 use riscv64 as arch;
48 } else {
49 compile_error!("unsupported architecture");
50 }
51}
52
53/// A WebAssembly stack trace.
54#[derive(Debug)]
55pub struct Backtrace(Vec<Frame>);
56
57/// A stack frame within a Wasm stack trace.
58#[derive(Debug)]
59pub struct Frame {
60 pc: usize,
61 fp: usize,
62}
63
64impl Frame {
65 /// Get this frame's program counter.
66 pub fn pc(&self) -> usize {
67 self.pc
68 }
69
70 /// Get this frame's frame pointer.
71 pub fn fp(&self) -> usize {
72 self.fp
73 }
74}
75
76impl Backtrace {
77 /// Returns an empty backtrace
78 pub fn empty() -> Backtrace {
79 Backtrace(Vec::new())
80 }
81
82 /// Capture the current Wasm stack in a backtrace.
83 pub fn new() -> Backtrace {
84 tls::with(|state| match state {
85 Some(state) => unsafe { Self::new_with_trap_state(state, None) },
86 None => Backtrace(vec![]),
87 })
88 }
89
90 /// Capture the current Wasm stack trace.
91 ///
92 /// If Wasm hit a trap, and we calling this from the trap handler, then the
93 /// Wasm exit trampoline didn't run, and we use the provided PC and FP
94 /// instead of looking them up in `VMRuntimeLimits`.
95 pub(crate) unsafe fn new_with_trap_state(
96 state: &CallThreadState,
97 trap_pc_and_fp: Option<(usize, usize)>,
98 ) -> Backtrace {
99 let mut frames = vec![];
100 Self::trace_with_trap_state(state, trap_pc_and_fp, |frame| {
101 frames.push(frame);
102 ControlFlow::Continue(())
103 });
104 Backtrace(frames)
105 }
106
107 /// Walk the current Wasm stack, calling `f` for each frame we walk.
108 pub fn trace(f: impl FnMut(Frame) -> ControlFlow<()>) {
109 tls::with(|state| match state {
110 Some(state) => unsafe { Self::trace_with_trap_state(state, None, f) },
111 None => {}
112 });
113 }
114
115 /// Walk the current Wasm stack, calling `f` for each frame we walk.
116 ///
117 /// If Wasm hit a trap, and we calling this from the trap handler, then the
118 /// Wasm exit trampoline didn't run, and we use the provided PC and FP
119 /// instead of looking them up in `VMRuntimeLimits`.
120 pub(crate) unsafe fn trace_with_trap_state(
121 state: &CallThreadState,
122 trap_pc_and_fp: Option<(usize, usize)>,
123 mut f: impl FnMut(Frame) -> ControlFlow<()>,
124 ) {
125 log::trace!("====== Capturing Backtrace ======");
126 let (last_wasm_exit_pc, last_wasm_exit_fp) = match trap_pc_and_fp {
127 // If we exited Wasm by catching a trap, then the Wasm-to-host
128 // trampoline did not get a chance to save the last Wasm PC and FP,
129 // and we need to use the plumbed-through values instead.
130 Some((pc, fp)) => (pc, fp),
131 // Either there is no Wasm currently on the stack, or we exited Wasm
132 // through the Wasm-to-host trampoline.
133 None => {
134 let pc = *(*state.limits).last_wasm_exit_pc.get();
135 let fp = *(*state.limits).last_wasm_exit_fp.get();
136 assert_ne!(pc, 0);
137 (pc, fp)
138 }
139 };
140
141 // Trace through the first contiguous sequence of Wasm frames on the
142 // stack.
143 if let ControlFlow::Break(()) = Self::trace_through_wasm(
144 last_wasm_exit_pc,
145 last_wasm_exit_fp,
146 *(*state.limits).last_wasm_entry_sp.get(),
147 &mut f,
148 ) {
149 log::trace!("====== Done Capturing Backtrace ======");
150 return;
151 }
152
153 // And then trace through each of the older contiguous sequences of Wasm
154 // frames on the stack.
155 for state in state.iter() {
156 // If there is no previous call state, then there is nothing more to
157 // trace through (since each `CallTheadState` saves the *previous*
158 // call into Wasm's saved registers, and the youngest call into
159 // Wasm's registers are saved in the `VMRuntimeLimits`)
160 if state.prev().is_null() {
161 debug_assert_eq!(state.old_last_wasm_exit_pc(), 0);
162 debug_assert_eq!(state.old_last_wasm_exit_fp(), 0);
163 debug_assert_eq!(state.old_last_wasm_entry_sp(), 0);
164 log::trace!("====== Done Capturing Backtrace ======");
165 return;
166 }
167
168 if let ControlFlow::Break(()) = Self::trace_through_wasm(
169 state.old_last_wasm_exit_pc(),
170 state.old_last_wasm_exit_fp(),
171 state.old_last_wasm_entry_sp(),
172 &mut f,
173 ) {
174 log::trace!("====== Done Capturing Backtrace ======");
175 return;
176 }
177 }
178
179 unreachable!()
180 }
181
182 /// Walk through a contiguous sequence of Wasm frames starting with the
183 /// frame at the given PC and FP and ending at `first_wasm_sp`.
184 unsafe fn trace_through_wasm(
185 mut pc: usize,
186 mut fp: usize,
187 first_wasm_sp: usize,
188 mut f: impl FnMut(Frame) -> ControlFlow<()>,
189 ) -> ControlFlow<()> {
190 log::trace!("=== Tracing through contiguous sequence of Wasm frames ===");
191 log::trace!("first_wasm_sp = 0x{:016x}", first_wasm_sp);
192 log::trace!(" initial pc = 0x{:016x}", pc);
193 log::trace!(" initial fp = 0x{:016x}", fp);
194
195 // In our host-to-Wasm trampoline, we save `-1` as a sentinal SP
196 // value for when the callee is not actually a core Wasm
197 // function (as determined by looking at the callee `vmctx`). If
198 // we encounter `-1`, this is an empty sequence of Wasm frames
199 // where a host called a host function so the following
200 // happened:
201 //
202 // * We entered the host-to-wasm-trampoline, saved (an invalid
203 // sentinal for) entry SP, and tail called to the "Wasm"
204 // callee,
205 //
206 // * entered the Wasm-to-host trampoline, saved the exit FP and
207 // PC, and tail called to the host callee,
208 //
209 // * and are now in host code.
210 //
211 // Ultimately, this means that there are 0 Wasm frames in this
212 // contiguous sequence of Wasm frames, and we have nothing to
213 // walk through here.
214 if first_wasm_sp == -1_isize as usize {
215 log::trace!("=== Done tracing (empty sequence of Wasm frames) ===");
216 return ControlFlow::Continue(());
217 }
218
219 // We use `0` as a sentinal value for when there is not any Wasm
220 // on the stack and these values are non-existant. If we
221 // actually entered Wasm (see above guard for `-1`) then, then
222 // by the time we got here we should have either exited Wasm
223 // through the Wasm-to-host trampoline and properly set these
224 // values, or we should have caught a trap in a signal handler
225 // and also properly recovered these values in that case.
226 assert_ne!(pc, 0);
227 assert_ne!(fp, 0);
228 assert_ne!(first_wasm_sp, 0);
229
230 // The stack grows down, and therefore any frame pointer we are
231 // dealing with should be less than the stack pointer on entry
232 // to Wasm.
233 assert!(first_wasm_sp >= fp, "{first_wasm_sp:#x} >= {fp:#x}");
234
235 arch::assert_entry_sp_is_aligned(first_wasm_sp);
236
237 loop {
238 arch::assert_fp_is_aligned(fp);
239
240 log::trace!("--- Tracing through one Wasm frame ---");
241 log::trace!("pc = {:p}", pc as *const ());
242 log::trace!("fp = {:p}", fp as *const ());
243
244 f(Frame { pc, fp })?;
245
246 // If our FP has reached the SP upon entry to Wasm from the
247 // host, then we've successfully walked all the Wasm frames,
248 // and have now reached a host frame. We're done iterating
249 // through this contiguous sequence of Wasm frames.
250 if arch::reached_entry_sp(fp, first_wasm_sp) {
251 log::trace!("=== Done tracing contiguous sequence of Wasm frames ===");
252 return ControlFlow::Continue(());
253 }
254
255 // If we didn't return above, then we know we are still in a
256 // Wasm frame, and since Cranelift maintains frame pointers,
257 // we know that the FP isn't an arbitrary value and it is
258 // safe to dereference it to read the next PC/FP.
259
260 pc = arch::get_next_older_pc_from_fp(fp);
261
262 // We rely on this offset being zero for all supported architectures
263 // in `crates/cranelift/src/component/compiler.rs` when we set the
264 // Wasm exit FP. If this ever changes, we will need to update that
265 // code as well!
266 assert_eq!(arch::NEXT_OLDER_FP_FROM_FP_OFFSET, 0);
267
268 let next_older_fp = *(fp as *mut usize).add(arch::NEXT_OLDER_FP_FROM_FP_OFFSET);
269 // Because the stack always grows down, the older FP must be greater
270 // than the current FP.
271 assert!(next_older_fp > fp, "{next_older_fp:#x} > {fp:#x}");
272 fp = next_older_fp;
273 }
274 }
275
276 /// Iterate over the frames inside this backtrace.
277 pub fn frames<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a Frame> + 'a {
278 self.0.iter()
279 }
280}