//! 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/wasmtime/src/runtime/vm/trampolines`). The most recent entry is
//! stored in `VMStoreContext` 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::prelude::*;
use crate::runtime::store::StoreOpaque;
use crate::runtime::vm::stack_switching::VMStackChain;
use crate::runtime::vm::{
Unwind, VMStoreContext,
traphandlers::{CallThreadState, tls},
};
#[cfg(all(feature = "gc", feature = "stack-switching"))]
use crate::vm::stack_switching::{VMContRef, VMStackState};
use core::ops::ControlFlow;
use wasmtime_unwinder::Frame;
#[cfg(feature = "debug")]
use wasmtime_unwinder::FrameCursor;
/// A WebAssembly stack trace.
#[derive(Debug)]
pub struct Backtrace(Vec);
/// One activation: information sufficient to trace an activation on a
/// frame as long as that frame remains alive.
pub(crate) struct Activation {
exit_pc: usize,
exit_fp: usize,
entry_trampoline_fp: usize,
}
impl Activation {
/// Create a frame cursor starting at the exit frame of this activation.
///
/// # Safety
///
/// This activation must currently be valid (i.e., execution must
/// not have returned into the activation to unwind any frames,
/// and the stack must not have been freed).
#[cfg(feature = "debug")]
pub(crate) unsafe fn cursor(&self) -> FrameCursor {
// SAFETY: validity of this activation is ensured by our
// safety condition.
unsafe { FrameCursor::new(self.exit_pc, self.exit_fp, self.entry_trampoline_fp) }
}
}
impl Backtrace {
/// Returns an empty backtrace
pub fn empty() -> Backtrace {
Backtrace(Vec::new())
}
/// Capture the current Wasm stack in a backtrace.
pub fn new(store: &StoreOpaque) -> Backtrace {
let vm_store_context = store.vm_store_context();
let unwind = store.unwinder();
tls::with(|state| match state {
Some(state) => unsafe {
Self::new_with_trap_state(vm_store_context, unwind, 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 `VMStoreContext`.
pub(crate) unsafe fn new_with_trap_state(
vm_store_context: *const VMStoreContext,
unwind: &dyn Unwind,
state: &CallThreadState,
trap_pc_and_fp: Option<(usize, usize)>,
) -> Backtrace {
let mut frames = vec![];
let f = |activation: Activation| unsafe {
wasmtime_unwinder::visit_frames(
unwind,
activation.exit_pc,
activation.exit_fp,
activation.entry_trampoline_fp,
|frame| {
frames.push(frame);
ControlFlow::Continue(())
},
)
};
unsafe {
Self::trace_with_trap_state(vm_store_context, state, trap_pc_and_fp, f);
}
Backtrace(frames)
}
/// Walk the current Wasm stack, calling `f` for each frame we walk.
#[cfg(feature = "gc")]
pub fn trace(store: &StoreOpaque, mut f: impl FnMut(Frame) -> ControlFlow<()>) {
let vm_store_context = store.vm_store_context();
let unwind = store.unwinder();
tls::with(|state| match state {
Some(state) => unsafe {
let f = |activation: Activation| {
wasmtime_unwinder::visit_frames(
unwind,
activation.exit_pc,
activation.exit_fp,
activation.entry_trampoline_fp,
&mut f,
)
};
Self::trace_with_trap_state(vm_store_context, state, None, f)
},
None => {}
});
}
// Walk the stack of the given continuation, which must be suspended, and
// all of its parent continuations (if any).
#[cfg(all(feature = "gc", feature = "stack-switching"))]
pub fn trace_suspended_continuation(
store: &StoreOpaque,
continuation: &VMContRef,
mut f: impl FnMut(Frame) -> ControlFlow<()>,
) {
log::trace!("====== Capturing Backtrace (suspended continuation) ======");
assert_eq!(
continuation.common_stack_information.state,
VMStackState::Suspended
);
let unwind = store.unwinder();
let pc = continuation.stack.control_context_instruction_pointer();
let fp = continuation.stack.control_context_frame_pointer();
let trampoline_fp = continuation
.common_stack_information
.limits
.last_wasm_entry_fp;
unsafe {
// FIXME(frank-emrich) Casting from *const to *mut pointer is
// terrible, but we won't actually modify any of the continuations
// here.
let stack_chain =
VMStackChain::Continuation(continuation as *const VMContRef as *mut VMContRef);
if let ControlFlow::Break(()) = Self::trace_through_continuations(
stack_chain,
pc,
fp,
trampoline_fp,
|activation| {
wasmtime_unwinder::visit_frames(
unwind,
activation.exit_pc,
activation.exit_fp,
activation.entry_trampoline_fp,
&mut f,
)
},
) {
log::trace!("====== Done Capturing Backtrace (closure break) ======");
return;
}
}
log::trace!("====== Done Capturing Backtrace (reached end of stack chain) ======");
}
/// 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 `VMStoreContext`.
///
/// We define "current Wasm stack" here as "all activations
/// associated with the given store". That is: if we have a stack like
///
/// ```plain
/// host --> (Wasm functions in store A) --> host --> (Wasm functions in store B) --> host
/// --> (Wasm functions in store A) --> host --> call `trace_with_trap_state` with store A
/// ```
///
/// then we will see the first and third Wasm activations (those
/// associated with store A), but not that with store B. In
/// essence, activations from another store might as well be some
/// other opaque host code; we don't know anything about it.
pub(crate) unsafe fn trace_with_trap_state(
vm_store_context: *const VMStoreContext,
state: &CallThreadState,
trap_pc_and_fp: Option<(usize, usize)>,
mut f: impl FnMut(Activation) -> ControlFlow<()>,
) {
log::trace!("====== Capturing Backtrace ======");
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)) => {
assert!(core::ptr::eq(
vm_store_context,
state.vm_store_context.get().as_ptr()
));
(pc, fp)
}
// Either there is no Wasm currently on the stack, or we exited Wasm
// through the Wasm-to-host trampoline.
None => unsafe {
let pc = *(*vm_store_context).last_wasm_exit_pc.get();
let fp = (*vm_store_context).last_wasm_exit_fp();
(pc, fp)
},
};
let stack_chain = unsafe { (*(*vm_store_context).stack_chain.get()).clone() };
// The first value in `activations` is for the most recently running
// wasm. We thus provide the stack chain of `first_wasm_state` to
// traverse the potential continuation stacks. For the subsequent
// activations, we unconditionally use `None` as the corresponding stack
// chain. This is justified because only the most recent execution of
// wasm may execute off the initial stack (see comments in
// `wasmtime::invoke_wasm_and_catch_traps` for details).
let activations =
core::iter::once((stack_chain, last_wasm_exit_pc, last_wasm_exit_fp, unsafe {
*(*vm_store_context).last_wasm_entry_fp.get()
}))
.chain(
state
.iter()
.flat_map(|state| state.iter())
.filter(|state| {
core::ptr::eq(vm_store_context, state.vm_store_context.get().as_ptr())
})
.map(|state| unsafe {
(
state.old_stack_chain(),
state.old_last_wasm_exit_pc(),
state.old_last_wasm_exit_fp(),
state.old_last_wasm_entry_fp(),
)
}),
)
.take_while(|(chain, pc, fp, sp)| {
if *pc == 0 {
debug_assert_eq!(*fp, 0);
debug_assert_eq!(*sp, 0);
} else {
debug_assert_ne!(chain.clone(), VMStackChain::Absent)
}
*pc != 0
});
for (chain, exit_pc, exit_fp, entry_trampoline_fp) in activations {
let res = unsafe {
Self::trace_through_continuations(
chain,
exit_pc,
exit_fp,
entry_trampoline_fp,
&mut f,
)
};
if let ControlFlow::Break(()) = res {
log::trace!("====== Done Capturing Backtrace (closure break) ======");
return;
}
}
log::trace!("====== Done Capturing Backtrace (reached end of activations) ======");
}
/// Traces through a sequence of stacks, creating a backtrace for each one,
/// beginning at the given `pc` and `fp`.
///
/// If `chain` is `InitialStack`, we are tracing through the initial stack,
/// and this function behaves like `trace_through_wasm`.
/// Otherwise, we can interpret `chain` as a linked list of stacks, which
/// ends with the initial stack. We then trace through each of these stacks
/// individually, up to (and including) the initial stack.
unsafe fn trace_through_continuations(
chain: VMStackChain,
exit_pc: usize,
exit_fp: usize,
entry_trampoline_fp: usize,
mut f: impl FnMut(Activation) -> ControlFlow<()>,
) -> ControlFlow<()> {
use crate::runtime::vm::stack_switching::{VMContRef, VMStackLimits};
// Handle the stack that is currently running (which may be a
// continuation or the initial stack).
f(Activation {
exit_pc,
exit_fp,
entry_trampoline_fp,
})?;
// Note that the rest of this function has no effect if `chain` is
// `Some(VMStackChain::InitialStack(_))` (i.e., there is only one stack to
// trace through: the initial stack)
assert_ne!(chain, VMStackChain::Absent);
let stack_limits_vec: Vec<*mut VMStackLimits> =
unsafe { chain.clone().into_stack_limits_iter().collect() };
let continuations_vec: Vec<*mut VMContRef> =
unsafe { chain.clone().into_continuation_iter().collect() };
// The VMStackLimits of the currently running stack (whether that's a
// continuation or the initial stack) contains undefined data, the
// information about that stack is saved in the Store's
// `VMStoreContext` and handled at the top of this function
// already. That's why we ignore `stack_limits_vec[0]`.
//
// Note that a continuation stack's control context stores
// information about how to resume execution *in its parent*. Thus,
// we combine the information from continuations_vec[i] with
// stack_limits_vec[i + 1] below to get information about a
// particular stack.
//
// There must be exactly one more `VMStackLimits` object than there
// are continuations, due to the initial stack having one, too.
assert_eq!(stack_limits_vec.len(), continuations_vec.len() + 1);
for i in 0..continuations_vec.len() {
// The continuation whose control context we want to
// access, to get information about how to continue
// execution in its parent.
let continuation = unsafe { &*continuations_vec[i] };
// The stack limits describing the parent of `continuation`.
let parent_limits = unsafe { &*stack_limits_vec[i + 1] };
// The parent of `continuation` if present not the last in the chain.
let parent_continuation = continuations_vec.get(i + 1).map(|&c| unsafe { &*c });
let fiber_stack = continuation.fiber_stack();
let resume_pc = fiber_stack.control_context_instruction_pointer();
let resume_fp = fiber_stack.control_context_frame_pointer();
// If the parent is indeed a continuation, we know the
// boundaries of its stack and can perform some extra debugging
// checks.
let parent_stack_range = parent_continuation.and_then(|p| p.fiber_stack().range());
parent_stack_range.inspect(|parent_stack_range| {
debug_assert!(parent_stack_range.contains(&resume_fp));
debug_assert!(parent_stack_range.contains(&parent_limits.last_wasm_entry_fp));
debug_assert!(parent_stack_range.contains(&parent_limits.stack_limit));
});
f(Activation {
exit_pc: resume_pc,
exit_fp: resume_fp,
entry_trampoline_fp: parent_limits.last_wasm_entry_fp,
})?;
}
ControlFlow::Continue(())
}
/// Capture all Activations reachable from the current point
/// within a hostcall.
#[cfg(feature = "debug")]
pub(crate) fn activations(store: &StoreOpaque) -> Vec {
let mut activations = vec![];
let vm_store_context = store.vm_store_context();
tls::with(|state| match state {
Some(state) => unsafe {
Self::trace_with_trap_state(vm_store_context, state, None, |act| {
activations.push(act);
ControlFlow::Continue(())
});
},
None => {}
});
activations
}
/// Iterate over the frames inside this backtrace.
pub fn frames<'a>(
&'a self,
) -> impl ExactSizeIterator- + DoubleEndedIterator + 'a {
self.0.iter()
}
}