//! Backtrace and stack walking functionality for Wasm. //! //! Walking the Wasm stack is comprised of //! //! 1. identifying sequences of contiguous Wasm frames on the stack //! (i.e. skipping over native host frames), and //! //! 2. walking the Wasm frames within such a sequence. //! //! To perform (1) we maintain the entry stack pointer (SP) and exit frame //! pointer (FP) and program counter (PC) each time we call into Wasm and Wasm //! calls into the host via trampolines (see //! `crates/runtime/src/trampolines`). The most recent entry is stored in //! `VMRuntimeLimits` and older entries are saved in `CallThreadState`. This //! lets us identify ranges of contiguous Wasm frames on the stack. //! //! To solve (2) and walk the Wasm frames within a region of contiguous Wasm //! frames on the stack, we configure Cranelift's `preserve_frame_pointers = //! true` setting. Then we can do simple frame pointer traversal starting at the //! exit FP and stopping once we reach the entry SP (meaning that the next older //! frame is a host frame). use crate::traphandlers::{tls, CallThreadState}; use cfg_if::cfg_if; use std::ops::ControlFlow; // Architecture-specific bits for stack walking. Each of these modules should // define and export the following functions: // // * `unsafe fn get_next_older_pc_from_fp(fp: usize) -> usize` // * `unsafe fn get_next_older_fp_from_fp(fp: usize) -> usize` // * `fn reached_entry_sp(fp: usize, first_wasm_sp: usize) -> bool` // * `fn assert_entry_sp_is_aligned(sp: usize)` // * `fn assert_fp_is_aligned(fp: usize)` cfg_if! { if #[cfg(target_arch = "x86_64")] { mod x86_64; use x86_64 as arch; } else if #[cfg(target_arch = "aarch64")] { mod aarch64; use aarch64 as arch; } else if #[cfg(target_arch = "s390x")] { mod s390x; use s390x as arch; } else { compile_error!("unsupported architecture"); } } /// A WebAssembly stack trace. #[derive(Debug)] pub struct Backtrace(Vec); /// A stack frame within a Wasm stack trace. #[derive(Debug)] pub struct Frame { pc: usize, fp: usize, } impl Frame { /// Get this frame's program counter. pub fn pc(&self) -> usize { self.pc } /// Get this frame's frame pointer. pub fn fp(&self) -> usize { self.fp } } impl Backtrace { /// Capture the current Wasm stack in a backtrace. pub fn new() -> Backtrace { tls::with(|state| match state { Some(state) => unsafe { Self::new_with_trap_state(state, None) }, None => Backtrace(vec![]), }) } /// Capture the current Wasm stack trace. /// /// If Wasm hit a trap, and we calling this from the trap handler, then the /// Wasm exit trampoline didn't run, and we use the provided PC and FP /// instead of looking them up in `VMRuntimeLimits`. pub(crate) unsafe fn new_with_trap_state( state: &CallThreadState, trap_pc_and_fp: Option<(usize, usize)>, ) -> Backtrace { let mut frames = vec![]; Self::trace_with_trap_state(state, trap_pc_and_fp, |frame| { frames.push(frame); ControlFlow::Continue(()) }); Backtrace(frames) } /// Walk the current Wasm stack, calling `f` for each frame we walk. pub fn trace(f: impl FnMut(Frame) -> ControlFlow<()>) { tls::with(|state| match state { Some(state) => unsafe { Self::trace_with_trap_state(state, None, f) }, None => {} }); } /// Walk the current Wasm stack, calling `f` for each frame we walk. /// /// If Wasm hit a trap, and we calling this from the trap handler, then the /// Wasm exit trampoline didn't run, and we use the provided PC and FP /// instead of looking them up in `VMRuntimeLimits`. pub(crate) unsafe fn trace_with_trap_state( state: &CallThreadState, trap_pc_and_fp: Option<(usize, usize)>, mut f: impl FnMut(Frame) -> ControlFlow<()>, ) { let (last_wasm_exit_pc, last_wasm_exit_fp) = match trap_pc_and_fp { // If we exited Wasm by catching a trap, then the Wasm-to-host // trampoline did not get a chance to save the last Wasm PC and FP, // and we need to use the plumbed-through values instead. Some((pc, fp)) => (pc, fp), // Either there is no Wasm currently on the stack, or we exited Wasm // through the Wasm-to-host trampoline. None => { let pc = *(*state.limits).last_wasm_exit_pc.get(); let fp = *(*state.limits).last_wasm_exit_fp.get(); assert_ne!(pc, 0); (pc, fp) } }; // Trace through the first contiguous sequence of Wasm frames on the // stack. if let ControlFlow::Break(()) = Self::trace_through_wasm( last_wasm_exit_pc, last_wasm_exit_fp, *(*state.limits).last_wasm_entry_sp.get(), &mut f, ) { return; } // And then trace through each of the older contiguous sequences of Wasm // frames on the stack. for state in state.iter() { // If there is no previous call state, then there is nothing more to // trace through (since each `CallTheadState` saves the *previous* // call into Wasm's saved registers, and the youngest call into // Wasm's registers are saved in the `VMRuntimeLimits`) if state.prev.get().is_null() { debug_assert_eq!(state.old_last_wasm_exit_pc, 0); debug_assert_eq!(state.old_last_wasm_exit_fp, 0); debug_assert_eq!(state.old_last_wasm_entry_sp, 0); return; } if let ControlFlow::Break(()) = Self::trace_through_wasm( state.old_last_wasm_exit_pc, state.old_last_wasm_exit_fp, state.old_last_wasm_entry_sp, &mut f, ) { return; } } } /// Walk through a contiguous sequence of Wasm frames starting with the /// frame at the given PC and FP and ending at `first_wasm_sp`. unsafe fn trace_through_wasm( mut pc: usize, mut fp: usize, first_wasm_sp: usize, mut f: impl FnMut(Frame) -> ControlFlow<()>, ) -> ControlFlow<()> { log::trace!("=== Tracing through contiguous sequence of Wasm frames ==="); log::trace!("first_wasm_sp = 0x{:016x}", first_wasm_sp); log::trace!(" initial pc = 0x{:016x}", pc); log::trace!(" initial fp = 0x{:016x}", fp); // In our host-to-Wasm trampoline, we save `-1` as a sentinal SP // value for when the callee is not actually a core Wasm // function (as determined by looking at the callee `vmctx`). If // we encounter `-1`, this is an empty sequence of Wasm frames // where a host called a host function so the following // happened: // // * We entered the host-to-wasm-trampoline, saved (an invalid // sentinal for) entry SP, and tail called to the "Wasm" // callee, // // * entered the Wasm-to-host trampoline, saved the exit FP and // PC, and tail called to the host callee, // // * and are now in host code. // // Ultimately, this means that there are 0 Wasm frames in this // contiguous sequence of Wasm frames, and we have nothing to // walk through here. if first_wasm_sp == -1_isize as usize { log::trace!("Empty sequence of Wasm frames"); return ControlFlow::Continue(()); } // We use `0` as a sentinal value for when there is not any Wasm // on the stack and these values are non-existant. If we // actually entered Wasm (see above guard for `-1`) then, then // by the time we got here we should have either exited Wasm // through the Wasm-to-host trampoline and properly set these // values, or we should have caught a trap in a signal handler // and also properly recovered these values in that case. assert_ne!(pc, 0); assert_ne!(fp, 0); assert_ne!(first_wasm_sp, 0); // The stack grows down, and therefore any frame pointer we are // dealing with should be less than the stack pointer on entry // to Wasm. assert!(first_wasm_sp >= fp, "{first_wasm_sp:#x} >= {fp:#x}"); arch::assert_entry_sp_is_aligned(first_wasm_sp); loop { arch::assert_fp_is_aligned(fp); log::trace!("--- Tracing through one Wasm frame ---"); log::trace!("pc = 0x{:016x}", pc); log::trace!("fp = 0x{:016x}", fp); f(Frame { pc, fp })?; // If our FP has reached the SP upon entry to Wasm from the // host, then we've successfully walked all the Wasm frames, // and have now reached a host frame. We're done iterating // through this contiguous sequence of Wasm frames. if arch::reached_entry_sp(fp, first_wasm_sp) { return ControlFlow::Continue(()); } // If we didn't return above, then we know we are still in a // Wasm frame, and since Cranelift maintains frame pointers, // we know that the FP isn't an arbitrary value and it is // safe to dereference it to read the next PC/FP. pc = arch::get_next_older_pc_from_fp(fp); let next_older_fp = arch::get_next_older_fp_from_fp(fp); // Because the stack always grows down, the older FP must be greater // than the current FP. assert!(next_older_fp > fp, "{next_older_fp:#x} > {fp:#x}"); fp = next_older_fp; } } /// Iterate over the frames inside this backtrace. pub fn frames<'a>(&'a self) -> impl Iterator + 'a { self.0.iter() } }