Path: blob/main/crates/unwinder/src/stackwalk.rs
3067 views
//! Stack-walking of a Wasm stack.1//!2//! A stack walk requires a first and last frame pointer (FP), and it3//! only works on code that has been compiled with frame pointers4//! enabled (`preserve_frame_pointers` Cranelift option enabled). The5//! stack walk follows the singly-linked list of saved frame pointer6//! and return address pairs on the stack that is naturally built by7//! function prologues.8//!9//! This crate makes use of the fact that Wasmtime surrounds Wasm10//! frames by trampolines both at entry and exit, and is "up the11//! stack" from the point doing the unwinding: in other words, host12//! code invokes Wasm code via an entry trampoline, that code may call13//! other Wasm code, and ultimately it calls back to host code via an14//! exit trampoline. That exit trampoline is able to provide the15//! "start FP" (FP at exit trampoline) and "end FP" (FP at entry16//! trampoline) and this stack-walker can visit all Wasm frames17//! active on the stack between those two.18//!19//! This module provides a visitor interface to frames, but is20//! agnostic to the desired use-case or consumer of the frames, and to21//! the overall runtime structure.2223use core::ops::ControlFlow;2425/// Implementation necessary to unwind the stack, used by `Backtrace`.26///27/// # Safety28///29/// This trait is `unsafe` because the return values of each function are30/// required to be semantically correct when connected to the `visit_frames`31/// function below. Incorrect and/or arbitrary values in this trait will cause32/// unwinding to segfault or otherwise result in UB.33pub unsafe trait Unwind {34/// Returns the offset, from the current frame pointer, of where to get to35/// the previous frame pointer on the stack.36fn next_older_fp_from_fp_offset(&self) -> usize;3738/// Returns the offset, from the current frame pointer, of the39/// stack pointer of the next older frame.40fn next_older_sp_from_fp_offset(&self) -> usize;4142/// Load the return address of a frame given the frame pointer for that43/// frame.44///45/// # Safety46///47/// This function is expected to read raw memory from `fp` and thus is not48/// safe to operate on any value of `fp` passed in, instead it must be a49/// trusted Cranelift-defined frame pointer.50unsafe fn get_next_older_pc_from_fp(&self, fp: usize) -> usize;5152/// Debug assertion that the frame pointer is aligned.53fn assert_fp_is_aligned(&self, fp: usize);54}5556/// A stack frame within a Wasm stack trace.57#[derive(Debug)]58pub struct Frame {59/// The program counter in this frame. Because every frame in the60/// stack-walk is paused at a call (as we are in host code called61/// by Wasm code below these frames), the PC is at the return62/// address, i.e., points to the instruction after the call63/// instruction.64pc: usize,65/// The frame pointer value corresponding to this frame.66fp: usize,67}6869impl Frame {70/// Get this frame's program counter.71pub fn pc(&self) -> usize {72self.pc73}7475/// Get this frame's frame pointer.76pub fn fp(&self) -> usize {77self.fp78}7980/// Read out a machine-word-sized value at the given offset from81/// FP in this frame.82///83/// # Safety84///85/// Requires that this frame is a valid, active frame. A `Frame`86/// provided by `visit_frames()` will be valid for the duration of87/// the invoked closure.88///89/// Requires that `offset` falls within the size of this90/// frame. This ordinarily requires knowledge passed from the91/// compiler that produced the running function, e.g., Cranelift.92pub unsafe fn read_slot_from_fp(&self, offset: isize) -> usize {93// SAFETY: we required that this is a valid frame, and that94// `offset` is a valid offset within that frame.95unsafe { *(self.fp.wrapping_add_signed(offset) as *mut usize) }96}97}9899/// Provide an iterator that walks through a contiguous sequence of100/// Wasm frames starting with the frame at the given PC and FP and101/// ending at `trampoline_fp`. This FP should correspond to that of a102/// trampoline that was used to enter the Wasm code.103///104/// We require that the initial PC, FP, and `trampoline_fp` values are105/// non-null (non-zero).106///107/// # Safety108///109/// This function is not safe as `unwind`, `pc`, `fp`, and `trampoline_fp` must110/// all be "correct" in that if they're wrong or mistakenly have the wrong value111/// then this method may segfault. These values must point to valid Wasmtime112/// compiled code which respects the frame pointers that Wasmtime currently113/// requires.114///115/// The iterator that this function returns *must* be consumed while116/// the frames are still active. That is, it cannot be stashed and117/// consumed after returning back into the Wasm activation that is118/// being iterated over.119///120/// Ordinarily this can be ensured by holding the unsafe iterator121/// together with a borrow of the `Store` that owns the stack;122/// higher-level layers wrap the two together.123pub unsafe fn frame_iterator(124unwind: &dyn Unwind,125mut pc: usize,126mut fp: usize,127trampoline_fp: usize,128) -> impl Iterator<Item = Frame> {129log::trace!("=== Tracing through contiguous sequence of Wasm frames ===");130log::trace!("trampoline_fp = 0x{trampoline_fp:016x}");131log::trace!(" initial pc = 0x{pc:016x}");132log::trace!(" initial fp = 0x{fp:016x}");133134// Safety requirements documented above.135assert_ne!(pc, 0);136assert_ne!(fp, 0);137assert_ne!(trampoline_fp, 0);138139// This loop will walk the linked list of frame pointers starting140// at `fp` and going up until `trampoline_fp`. We know that both141// `fp` and `trampoline_fp` are "trusted values" aka generated and142// maintained by Wasmtime. This means that it should be safe to143// walk the linked list of pointers and inspect Wasm frames.144//145// Note, though, that any frames outside of this range are not146// guaranteed to have valid frame pointers. For example native code147// might be using the frame pointer as a general purpose register. Thus148// we need to be careful to only walk frame pointers in this one149// contiguous linked list.150//151// To know when to stop iteration all architectures' stacks currently152// look something like this:153//154// | ... |155// | Native Frames |156// | ... |157// |-------------------|158// | ... | <-- Trampoline FP |159// | Trampoline Frame | |160// | ... | <-- Trampoline SP |161// |-------------------| Stack162// | Return Address | Grows163// | Previous FP | <-- Wasm FP Down164// | ... | |165// | Cranelift Frames | |166// | ... | V167//168// The trampoline records its own frame pointer (`trampoline_fp`),169// which is guaranteed to be above all Wasm code. To check when170171// to check when the next frame pointer is equal to172// `trampoline_fp`. Once that's hit then we know that the entire173// linked list has been traversed.174//175// Note that it might be possible that this loop doesn't execute176// at all. For example if the entry trampoline called Wasm code177// which `return_call`'d an exit trampoline, then `fp ==178// trampoline_fp` on the entry of this function, meaning the loop179// won't actually execute anything.180core::iter::from_fn(move || {181if fp == trampoline_fp {182log::trace!("=== Done tracing contiguous sequence of Wasm frames ===");183return None;184}185186// At the start of each iteration of the loop, we know that187// `fp` is a frame pointer from Wasm code. Therefore, we know188// it is not being used as an extra general-purpose register,189// and it is safe dereference to get the PC and the next older190// frame pointer.191//192// The stack also grows down, and therefore any frame pointer193// we are dealing with should be less than the frame pointer194// on entry to Wasm code. Finally also assert that it's195// aligned correctly as an additional sanity check.196assert!(trampoline_fp > fp, "{trampoline_fp:#x} > {fp:#x}");197unwind.assert_fp_is_aligned(fp);198199log::trace!("--- Tracing through one Wasm frame ---");200log::trace!("pc = {:p}", pc as *const ());201log::trace!("fp = {:p}", fp as *const ());202203let frame = Frame { pc, fp };204205// SAFETY: this unsafe traversal of the linked list on the stack is206// reflected in the contract of this function where `pc`, `fp`,207// `trampoline_fp`, and `unwind` must all be trusted/correct values.208unsafe {209pc = unwind.get_next_older_pc_from_fp(fp);210211// We rely on this offset being zero for all supported212// architectures in213// `crates/cranelift/src/component/compiler.s`r when we set214// the Wasm exit FP. If this ever changes, we will need to215// update that code as well!216assert_eq!(unwind.next_older_fp_from_fp_offset(), 0);217218// Get the next older frame pointer from the current Wasm219// frame pointer.220let next_older_fp = *(fp as *mut usize).add(unwind.next_older_fp_from_fp_offset());221222// Because the stack always grows down, the older FP must be greater223// than the current FP.224assert!(next_older_fp > fp, "{next_older_fp:#x} > {fp:#x}");225fp = next_older_fp;226}227228Some(frame)229})230}231232/// Walk through a contiguous sequence of Wasm frames starting with233/// the frame at the given PC and FP and ending at234/// `trampoline_fp`. This FP should correspond to that of a trampoline235/// that was used to enter the Wasm code.236///237/// We require that the initial PC, FP, and `trampoline_fp` values are238/// non-null (non-zero).239///240/// # Safety241///242/// This function is not safe as `unwind`, `pc`, `fp`, and `trampoline_fp` must243/// all be "correct" in that if they're wrong or mistakenly have the wrong value244/// then this method may segfault. These values must point to valid Wasmtime245/// compiled code which respects the frame pointers that Wasmtime currently246/// requires.247pub unsafe fn visit_frames<R>(248unwind: &dyn Unwind,249pc: usize,250fp: usize,251trampoline_fp: usize,252mut f: impl FnMut(Frame) -> ControlFlow<R>,253) -> ControlFlow<R> {254let iter = unsafe { frame_iterator(unwind, pc, fp, trampoline_fp) };255for frame in iter {256f(frame)?;257}258259ControlFlow::Continue(())260}261262263