use crate::Result;
use crate::{
AnyRef, AsContext, AsContextMut, CodeMemory, ExnRef, Extern, ExternRef, Func, Instance, Module,
OwnedRooted, StoreContext, StoreContextMut, Val,
code::StoreCodePC,
module::ModuleRegistry,
store::{AutoAssertNoGc, StoreOpaque},
vm::{CompiledModuleId, FrameOrHostCode, StoreBacktrace, VMContext},
};
use alloc::collections::BTreeSet;
use alloc::vec;
use alloc::vec::Vec;
use core::{ffi::c_void, ptr::NonNull};
#[cfg(feature = "gc")]
use wasmtime_environ::FrameTable;
use wasmtime_environ::{
DefinedFuncIndex, EntityIndex, FrameInstPos, FrameStackShape, FrameStateSlot,
FrameStateSlotOffset, FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType,
FuncIndex, FuncKey, GlobalIndex, MemoryIndex, TableIndex, TagIndex, Trap,
};
use wasmtime_unwinder::Frame;
use super::store::AsStoreOpaque;
impl<'a, T> StoreContextMut<'a, T> {
pub fn debug_frames(self) -> Option<DebugFrameCursor<'a, T>> {
if !self.engine().tunables().debug_guest {
return None;
}
let iter = StoreBacktrace::new(self);
let mut view = DebugFrameCursor {
iter,
is_trapping_frame: false,
frames: vec![],
current: None,
};
view.move_to_parent(); Some(view)
}
pub fn edit_breakpoints(self) -> Option<BreakpointEdit<'a>> {
if !self.engine().tunables().debug_guest {
return None;
}
let (breakpoints, registry) = self.0.breakpoints_and_registry_mut();
Some(breakpoints.edit(registry))
}
}
impl Instance {
pub fn debug_global(
&self,
mut store: impl AsContextMut,
global_index: u32,
) -> Option<crate::Global> {
self.debug_export(
store.as_context_mut().0,
GlobalIndex::from_bits(global_index).into(),
)
.and_then(|s| s.into_global())
}
pub fn debug_memory(
&self,
mut store: impl AsContextMut,
memory_index: u32,
) -> Option<crate::Memory> {
self.debug_export(
store.as_context_mut().0,
MemoryIndex::from_bits(memory_index).into(),
)
.and_then(|s| s.into_memory())
}
pub fn debug_shared_memory(
&self,
mut store: impl AsContextMut,
memory_index: u32,
) -> Option<crate::SharedMemory> {
self.debug_export(
store.as_context_mut().0,
MemoryIndex::from_bits(memory_index).into(),
)
.and_then(|s| s.into_shared_memory())
}
pub fn debug_table(
&self,
mut store: impl AsContextMut,
table_index: u32,
) -> Option<crate::Table> {
self.debug_export(
store.as_context_mut().0,
TableIndex::from_bits(table_index).into(),
)
.and_then(|s| s.into_table())
}
pub fn debug_function(
&self,
mut store: impl AsContextMut,
function_index: u32,
) -> Option<crate::Func> {
self.debug_export(
store.as_context_mut().0,
FuncIndex::from_bits(function_index).into(),
)
.and_then(|s| s.into_func())
}
pub fn debug_tag(&self, mut store: impl AsContextMut, tag_index: u32) -> Option<crate::Tag> {
self.debug_export(
store.as_context_mut().0,
TagIndex::from_bits(tag_index).into(),
)
.and_then(|s| s.into_tag())
}
fn debug_export(&self, store: &mut StoreOpaque, index: EntityIndex) -> Option<Extern> {
if !store.engine().tunables().debug_guest {
return None;
}
let env_module = self._module(store).env_module();
if !env_module.is_valid(index) {
return None;
}
let store_id = store.id();
let (instance, registry) = store.instance_and_module_registry_mut(self.id());
let export = unsafe { instance.get_export_by_index_mut(registry, store_id, index) };
Some(Extern::from_wasmtime_export(export, store))
}
}
impl<'a, T> StoreContext<'a, T> {
pub fn breakpoints(self) -> Option<impl Iterator<Item = Breakpoint> + 'a> {
if !self.engine().tunables().debug_guest {
return None;
}
let (breakpoints, registry) = self.0.breakpoints_and_registry();
Some(breakpoints.breakpoints(registry))
}
pub fn is_single_step(&self) -> bool {
let (breakpoints, _) = self.0.breakpoints_and_registry();
breakpoints.is_single_step()
}
}
pub struct DebugFrameCursor<'a, T: 'static> {
iter: StoreBacktrace<'a, T>,
is_trapping_frame: bool,
frames: Vec<VirtualFrame>,
current: Option<FrameData>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FrameParentResult {
SameActivation,
NewActivation,
}
impl<'a, T: 'static> DebugFrameCursor<'a, T> {
pub fn move_to_parent(&mut self) -> FrameParentResult {
let mut result = FrameParentResult::SameActivation;
self.current = None;
while self.frames.is_empty() {
let Some(next_frame) = self.iter.next() else {
return result;
};
self.frames = match next_frame {
FrameOrHostCode::Frame(frame) => VirtualFrame::decode(
self.iter.store_mut().0.as_store_opaque(),
frame,
self.is_trapping_frame,
),
FrameOrHostCode::HostCode => {
result = FrameParentResult::NewActivation;
continue;
}
};
debug_assert!(!self.frames.is_empty());
self.is_trapping_frame = false;
}
self.current = self.frames.pop().map(|vf| FrameData::compute(vf));
result
}
pub fn done(&self) -> bool {
self.current.is_none()
}
fn frame_data(&self) -> &FrameData {
self.current.as_ref().expect("No current frame")
}
fn raw_instance(&self) -> &crate::vm::Instance {
let vmctx: *mut VMContext = unsafe { *(self.frame_data().slot_addr as *mut _) };
let vmctx = NonNull::new(vmctx).expect("null vmctx in debug state slot");
let instance = unsafe { crate::vm::Instance::from_vmctx(vmctx) };
unsafe { instance.as_ref() }
}
pub fn instance(&mut self) -> Instance {
let instance = self.raw_instance();
Instance::from_wasmtime(instance.id(), self.iter.store_mut().0.as_store_opaque())
}
pub fn module(&self) -> Option<&Module> {
let instance = self.raw_instance();
instance.runtime_module()
}
pub fn wasm_function_index_and_pc(&self) -> Option<(DefinedFuncIndex, u32)> {
let data = self.frame_data();
let FuncKey::DefinedWasmFunction(module, func) = data.func_key else {
return None;
};
debug_assert_eq!(
module,
self.module()
.expect("module should be defined if this is a defined function")
.env_module()
.module_index
);
Some((func, data.wasm_pc))
}
pub fn num_locals(&self) -> u32 {
u32::try_from(self.frame_data().locals.len()).unwrap()
}
pub fn num_stacks(&self) -> u32 {
u32::try_from(self.frame_data().stack.len()).unwrap()
}
pub fn local(&mut self, index: u32) -> Val {
let data = self.frame_data();
let (offset, ty) = data.locals[usize::try_from(index).unwrap()];
let slot_addr = data.slot_addr;
unsafe { read_value(&mut self.iter.store_mut().0, slot_addr, offset, ty) }
}
pub fn stack(&mut self, index: u32) -> Val {
let data = self.frame_data();
let (offset, ty) = data.stack[usize::try_from(index).unwrap()];
let slot_addr = data.slot_addr;
unsafe { read_value(&mut self.iter.store_mut().0, slot_addr, offset, ty) }
}
}
struct VirtualFrame {
fp: *const u8,
module: Module,
wasm_pc: u32,
frame_descriptor: FrameTableDescriptorIndex,
stack_shape: FrameStackShape,
}
impl VirtualFrame {
fn decode(store: &mut StoreOpaque, frame: Frame, is_trapping_frame: bool) -> Vec<VirtualFrame> {
let (module_with_code, pc) = store
.modules()
.module_and_code_by_pc(frame.pc())
.expect("Wasm frame PC does not correspond to a module");
let module = module_with_code.module();
let table = module.frame_table().unwrap();
let pc = u32::try_from(pc).expect("PC offset too large");
let pos = if is_trapping_frame {
FrameInstPos::Pre
} else {
FrameInstPos::Post
};
let program_points = table.find_program_point(pc, pos).expect("There must be a program point record in every frame when debug instrumentation is enabled");
program_points
.map(|(wasm_pc, frame_descriptor, stack_shape)| VirtualFrame {
fp: core::ptr::with_exposed_provenance(frame.fp()),
module: module.clone(),
wasm_pc,
frame_descriptor,
stack_shape,
})
.collect()
}
}
struct FrameData {
slot_addr: *const u8,
func_key: FuncKey,
wasm_pc: u32,
locals: Vec<(FrameStateSlotOffset, FrameValType)>,
stack: Vec<(FrameStateSlotOffset, FrameValType)>,
}
impl FrameData {
fn compute(frame: VirtualFrame) -> Self {
let frame_table = frame.module.frame_table().unwrap();
let (data, slot_to_fp_offset) = frame_table
.frame_descriptor(frame.frame_descriptor)
.unwrap();
let frame_state_slot = FrameStateSlot::parse(data).unwrap();
let slot_addr = frame
.fp
.wrapping_sub(usize::try_from(slot_to_fp_offset).unwrap());
let mut stack = frame_state_slot
.stack(frame.stack_shape)
.collect::<Vec<_>>();
stack.reverse();
let locals = frame_state_slot.locals().collect::<Vec<_>>();
FrameData {
slot_addr,
func_key: frame_state_slot.func_key(),
wasm_pc: frame.wasm_pc,
stack,
locals,
}
}
}
unsafe fn read_value(
store: &mut StoreOpaque,
slot_base: *const u8,
offset: FrameStateSlotOffset,
ty: FrameValType,
) -> Val {
let address = unsafe { slot_base.offset(isize::try_from(offset.offset()).unwrap()) };
match ty {
FrameValType::I32 => {
let value = unsafe { *(address as *const i32) };
Val::I32(value)
}
FrameValType::I64 => {
let value = unsafe { *(address as *const i64) };
Val::I64(value)
}
FrameValType::F32 => {
let value = unsafe { *(address as *const u32) };
Val::F32(value)
}
FrameValType::F64 => {
let value = unsafe { *(address as *const u64) };
Val::F64(value)
}
FrameValType::V128 => {
let value = unsafe { *(address as *const u128) };
Val::V128(value.into())
}
FrameValType::AnyRef => {
let mut nogc = AutoAssertNoGc::new(store);
let value = unsafe { *(address as *const u32) };
let value = AnyRef::_from_raw(&mut nogc, value);
Val::AnyRef(value)
}
FrameValType::ExnRef => {
let mut nogc = AutoAssertNoGc::new(store);
let value = unsafe { *(address as *const u32) };
let value = ExnRef::_from_raw(&mut nogc, value);
Val::ExnRef(value)
}
FrameValType::ExternRef => {
let mut nogc = AutoAssertNoGc::new(store);
let value = unsafe { *(address as *const u32) };
let value = ExternRef::_from_raw(&mut nogc, value);
Val::ExternRef(value)
}
FrameValType::FuncRef => {
let value = unsafe { *(address as *const *mut c_void) };
let value = unsafe { Func::_from_raw(store, value) };
Val::FuncRef(value)
}
FrameValType::ContRef => {
unimplemented!("contref values are not implemented in the host API yet")
}
}
}
#[cfg(feature = "gc")]
pub(crate) fn gc_refs_in_frame<'a>(ft: FrameTable<'a>, pc: u32, fp: *mut usize) -> Vec<*mut u32> {
let fp = fp.cast::<u8>();
let mut ret = vec![];
if let Some(frames) = ft.find_program_point(pc, FrameInstPos::Post) {
for (_wasm_pc, frame_desc, stack_shape) in frames {
let (frame_desc_data, slot_to_fp_offset) = ft.frame_descriptor(frame_desc).unwrap();
let frame_base = unsafe { fp.offset(-isize::try_from(slot_to_fp_offset).unwrap()) };
let frame_desc = FrameStateSlot::parse(frame_desc_data).unwrap();
for (offset, ty) in frame_desc.stack_and_locals(stack_shape) {
match ty {
FrameValType::AnyRef | FrameValType::ExnRef | FrameValType::ExternRef => {
let slot = unsafe {
frame_base
.offset(isize::try_from(offset.offset()).unwrap())
.cast::<u32>()
};
ret.push(slot);
}
FrameValType::ContRef | FrameValType::FuncRef => {}
FrameValType::I32
| FrameValType::I64
| FrameValType::F32
| FrameValType::F64
| FrameValType::V128 => {}
}
}
}
}
ret
}
impl<'a, T: 'static> AsContext for DebugFrameCursor<'a, T> {
type Data = T;
fn as_context(&self) -> StoreContext<'_, Self::Data> {
StoreContext(self.iter.store().0)
}
}
impl<'a, T: 'static> AsContextMut for DebugFrameCursor<'a, T> {
fn as_context_mut(&mut self) -> StoreContextMut<'_, Self::Data> {
StoreContextMut(self.iter.store_mut().0)
}
}
#[derive(Debug)]
pub enum DebugEvent<'a> {
HostcallError(&'a crate::Error),
CaughtExceptionThrown(OwnedRooted<ExnRef>),
UncaughtExceptionThrown(OwnedRooted<ExnRef>),
Trap(Trap),
Breakpoint,
EpochYield,
}
pub trait DebugHandler: Clone + Send + Sync + 'static {
type Data;
fn handle(
&self,
store: StoreContextMut<'_, Self::Data>,
event: DebugEvent<'_>,
) -> impl Future<Output = ()> + Send;
}
#[derive(Default)]
pub(crate) struct BreakpointState {
single_step: bool,
breakpoints: BTreeSet<BreakpointKey>,
}
pub struct Breakpoint {
pub module: Module,
pub pc: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct BreakpointKey(CompiledModuleId, u32);
impl BreakpointKey {
fn from_raw(module: &Module, pc: u32) -> BreakpointKey {
BreakpointKey(module.id(), pc)
}
fn get(&self, registry: &ModuleRegistry) -> Breakpoint {
let module = registry
.module_by_compiled_id(self.0)
.expect("Module should not have been removed from Store")
.clone();
Breakpoint { module, pc: self.1 }
}
}
pub struct BreakpointEdit<'a> {
state: &'a mut BreakpointState,
registry: &'a mut ModuleRegistry,
dirty_modules: BTreeSet<StoreCodePC>,
}
impl BreakpointState {
pub(crate) fn edit<'a>(&'a mut self, registry: &'a mut ModuleRegistry) -> BreakpointEdit<'a> {
BreakpointEdit {
state: self,
registry,
dirty_modules: BTreeSet::new(),
}
}
pub(crate) fn breakpoints<'a>(
&'a self,
registry: &'a ModuleRegistry,
) -> impl Iterator<Item = Breakpoint> + 'a {
self.breakpoints.iter().map(|key| key.get(registry))
}
pub(crate) fn is_single_step(&self) -> bool {
self.single_step
}
}
impl<'a> BreakpointEdit<'a> {
fn get_code_memory<'b>(
registry: &'b mut ModuleRegistry,
dirty_modules: &mut BTreeSet<StoreCodePC>,
module: &Module,
) -> Result<&'b mut CodeMemory> {
let store_code_pc = registry.store_code_base_or_register(module)?;
let code_memory = registry
.store_code_mut(store_code_pc)
.expect("Just checked presence above")
.code_memory_mut()
.expect("Must have unique ownership of StoreCode in guest-debug mode");
if dirty_modules.insert(store_code_pc) {
code_memory.unpublish()?;
}
Ok(code_memory)
}
fn patch<'b>(
patches: impl Iterator<Item = FrameTableBreakpointData<'b>> + 'b,
mem: &mut CodeMemory,
enable: bool,
) {
let mem = mem.text_mut();
for patch in patches {
let data = if enable { patch.enable } else { patch.disable };
let mem = &mut mem[patch.offset..patch.offset + data.len()];
log::trace!(
"patch: offset 0x{:x} with enable={enable}: data {data:?} replacing {mem:?}",
patch.offset
);
mem.copy_from_slice(data);
}
}
pub fn add_breakpoint(&mut self, module: &Module, pc: u32) -> Result<()> {
let key = BreakpointKey::from_raw(module, pc);
self.state.breakpoints.insert(key);
log::trace!("patching in breakpoint {key:?}");
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, module)?;
let frame_table = module
.frame_table()
.expect("Frame table must be present when guest-debug is enabled");
let patches = frame_table.lookup_breakpoint_patches_by_pc(pc);
Self::patch(patches, mem, true);
Ok(())
}
pub fn remove_breakpoint(&mut self, module: &Module, pc: u32) -> Result<()> {
let key = BreakpointKey::from_raw(module, pc);
self.state.breakpoints.remove(&key);
if !self.state.single_step {
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, module)?;
let frame_table = module
.frame_table()
.expect("Frame table must be present when guest-debug is enabled");
let patches = frame_table.lookup_breakpoint_patches_by_pc(pc);
Self::patch(patches, mem, false);
}
Ok(())
}
pub fn single_step(&mut self, enabled: bool) -> Result<()> {
log::trace!(
"single_step({enabled}) with breakpoint set {:?}",
self.state.breakpoints
);
let modules = self.registry.all_modules().cloned().collect::<Vec<_>>();
for module in modules {
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, &module)?;
let table = module
.frame_table()
.expect("Frame table must be present when guest-debug is enabled");
for (wasm_pc, patch) in table.breakpoint_patches() {
let key = BreakpointKey::from_raw(&module, wasm_pc);
let this_enabled = enabled || self.state.breakpoints.contains(&key);
log::trace!(
"single_step: enabled {enabled} key {key:?} -> this_enabled {this_enabled}"
);
Self::patch(core::iter::once(patch), mem, this_enabled);
}
}
self.state.single_step = enabled;
Ok(())
}
}
impl<'a> Drop for BreakpointEdit<'a> {
fn drop(&mut self) {
for &store_code_base in &self.dirty_modules {
let store_code = self.registry.store_code_mut(store_code_base).unwrap();
if let Err(e) = store_code
.code_memory_mut()
.expect("Must have unique ownership of StoreCode in guest-debug mode")
.publish()
{
abort_on_republish_error(e);
}
}
}
}
#[cfg(feature = "std")]
fn abort_on_republish_error(e: crate::Error) -> ! {
log::error!(
"Failed to re-publish executable code: {e:?}. Wasmtime cannot return through JIT code on the stack and cannot even panic; aborting the process."
);
std::process::abort();
}
#[cfg(not(feature = "std"))]
fn abort_on_republish_error(e: crate::Error) -> ! {
panic!("Failed to re-publish executable code: {e:?}");
}