wasmtime_internal_unwinder/stackwalk.rs
1//! Stack-walking of a Wasm stack.
2//!
3//! A stack walk requires a first and last frame pointer (FP), and it
4//! only works on code that has been compiled with frame pointers
5//! enabled (`preserve_frame_pointers` Cranelift option enabled). The
6//! stack walk follows the singly-linked list of saved frame pointer
7//! and return address pairs on the stack that is naturally built by
8//! function prologues.
9//!
10//! This crate makes use of the fact that Wasmtime surrounds Wasm
11//! frames by trampolines both at entry and exit, and is "up the
12//! stack" from the point doing the unwinding: in other words, host
13//! code invokes Wasm code via an entry trampoline, that code may call
14//! other Wasm code, and ultimately it calls back to host code via an
15//! exit trampoline. That exit trampoline is able to provide the
16//! "start FP" (FP at exit trampoline) and "end FP" (FP at entry
17//! trampoline) and this stack-walker can visit all Wasm frames
18//! active on the stack between those two.
19//!
20//! This module provides a visitor interface to frames, but is
21//! agnostic to the desired use-case or consumer of the frames, and to
22//! the overall runtime structure.
23
24use core::ops::ControlFlow;
25
26/// Implementation necessary to unwind the stack, used by `Backtrace`.
27///
28/// # Safety
29///
30/// This trait is `unsafe` because the return values of each function are
31/// required to be semantically correct when connected to the `visit_frames`
32/// function below. Incorrect and/or arbitrary values in this trait will cause
33/// unwinding to segfault or otherwise result in UB.
34pub unsafe trait Unwind {
35 /// Returns the offset, from the current frame pointer, of where to get to
36 /// the previous frame pointer on the stack.
37 fn next_older_fp_from_fp_offset(&self) -> usize;
38
39 /// Returns the offset, from the current frame pointer, of the
40 /// stack pointer of the next older frame.
41 fn next_older_sp_from_fp_offset(&self) -> usize;
42
43 /// Load the return address of a frame given the frame pointer for that
44 /// frame.
45 ///
46 /// # Safety
47 ///
48 /// This function is expected to read raw memory from `fp` and thus is not
49 /// safe to operate on any value of `fp` passed in, instead it must be a
50 /// trusted Cranelift-defined frame pointer.
51 unsafe fn get_next_older_pc_from_fp(&self, fp: usize) -> usize;
52
53 /// Debug assertion that the frame pointer is aligned.
54 fn assert_fp_is_aligned(&self, fp: usize);
55}
56
57/// A stack frame within a Wasm stack trace.
58#[derive(Debug)]
59pub struct Frame {
60 /// The program counter in this frame. Because every frame in the
61 /// stack-walk is paused at a call (as we are in host code called
62 /// by Wasm code below these frames), the PC is at the return
63 /// address, i.e., points to the instruction after the call
64 /// instruction.
65 pc: usize,
66 /// The frame pointer value corresponding to this frame.
67 fp: usize,
68}
69
70impl Frame {
71 /// Get this frame's program counter.
72 pub fn pc(&self) -> usize {
73 self.pc
74 }
75
76 /// Get this frame's frame pointer.
77 pub fn fp(&self) -> usize {
78 self.fp
79 }
80}
81
82/// Walk through a contiguous sequence of Wasm frames starting with
83/// the frame at the given PC and FP and ending at
84/// `trampoline_fp`. This FP should correspond to that of a trampoline
85/// that was used to enter the Wasm code.
86///
87/// We require that the initial PC, FP, and `trampoline_fp` values are
88/// non-null (non-zero).
89///
90/// # Safety
91///
92/// This function is not safe as `unwind`, `pc`, `fp`, and `trampoline_fp` must
93/// all be "correct" in that if they're wrong or mistakenly have the wrong value
94/// then this method may segfault. These values must point to valid Wasmtime
95/// compiled code which respects the frame pointers that Wasmtime currently
96/// requires.
97pub unsafe fn visit_frames<R>(
98 unwind: &dyn Unwind,
99 mut pc: usize,
100 mut fp: usize,
101 trampoline_fp: usize,
102 mut f: impl FnMut(Frame) -> ControlFlow<R>,
103) -> ControlFlow<R> {
104 log::trace!("=== Tracing through contiguous sequence of Wasm frames ===");
105 log::trace!("trampoline_fp = 0x{:016x}", trampoline_fp);
106 log::trace!(" initial pc = 0x{:016x}", pc);
107 log::trace!(" initial fp = 0x{:016x}", fp);
108
109 // Safety requirements documented above.
110 assert_ne!(pc, 0);
111 assert_ne!(fp, 0);
112 assert_ne!(trampoline_fp, 0);
113
114 // This loop will walk the linked list of frame pointers starting
115 // at `fp` and going up until `trampoline_fp`. We know that both
116 // `fp` and `trampoline_fp` are "trusted values" aka generated and
117 // maintained by Wasmtime. This means that it should be safe to
118 // walk the linked list of pointers and inspect Wasm frames.
119 //
120 // Note, though, that any frames outside of this range are not
121 // guaranteed to have valid frame pointers. For example native code
122 // might be using the frame pointer as a general purpose register. Thus
123 // we need to be careful to only walk frame pointers in this one
124 // contiguous linked list.
125 //
126 // To know when to stop iteration all architectures' stacks currently
127 // look something like this:
128 //
129 // | ... |
130 // | Native Frames |
131 // | ... |
132 // |-------------------|
133 // | ... | <-- Trampoline FP |
134 // | Trampoline Frame | |
135 // | ... | <-- Trampoline SP |
136 // |-------------------| Stack
137 // | Return Address | Grows
138 // | Previous FP | <-- Wasm FP Down
139 // | ... | |
140 // | Cranelift Frames | |
141 // | ... | V
142 //
143 // The trampoline records its own frame pointer (`trampoline_fp`),
144 // which is guaranteed to be above all Wasm code. To check when
145 // we've reached the trampoline frame, it is therefore sufficient
146 // to check when the next frame pointer is equal to
147 // `trampoline_fp`. Once that's hit then we know that the entire
148 // linked list has been traversed.
149 //
150 // Note that it might be possible that this loop doesn't execute
151 // at all. For example if the entry trampoline called Wasm code
152 // which `return_call`'d an exit trampoline, then `fp ==
153 // trampoline_fp` on the entry of this function, meaning the loop
154 // won't actually execute anything.
155 while fp != trampoline_fp {
156 // At the start of each iteration of the loop, we know that
157 // `fp` is a frame pointer from Wasm code. Therefore, we know
158 // it is not being used as an extra general-purpose register,
159 // and it is safe dereference to get the PC and the next older
160 // frame pointer.
161 //
162 // The stack also grows down, and therefore any frame pointer
163 // we are dealing with should be less than the frame pointer
164 // on entry to Wasm code. Finally also assert that it's
165 // aligned correctly as an additional sanity check.
166 assert!(trampoline_fp > fp, "{trampoline_fp:#x} > {fp:#x}");
167 unwind.assert_fp_is_aligned(fp);
168
169 log::trace!("--- Tracing through one Wasm frame ---");
170 log::trace!("pc = {:p}", pc as *const ());
171 log::trace!("fp = {:p}", fp as *const ());
172
173 f(Frame { pc, fp })?;
174
175 // SAFETY: this unsafe traversal of the linked list on the stack is
176 // reflected in the contract of this function where `pc`, `fp`,
177 // `trampoline_fp`, and `unwind` must all be trusted/correct values.
178 unsafe {
179 pc = unwind.get_next_older_pc_from_fp(fp);
180
181 // We rely on this offset being zero for all supported
182 // architectures in
183 // `crates/cranelift/src/component/compiler.rs` when we set
184 // the Wasm exit FP. If this ever changes, we will need to
185 // update that code as well!
186 assert_eq!(unwind.next_older_fp_from_fp_offset(), 0);
187
188 // Get the next older frame pointer from the current Wasm
189 // frame pointer.
190 let next_older_fp = *(fp as *mut usize).add(unwind.next_older_fp_from_fp_offset());
191
192 // Because the stack always grows down, the older FP must be greater
193 // than the current FP.
194 assert!(next_older_fp > fp, "{next_older_fp:#x} > {fp:#x}");
195 fp = next_older_fp;
196 }
197 }
198
199 log::trace!("=== Done tracing contiguous sequence of Wasm frames ===");
200 ControlFlow::Continue(())
201}