wasmtime/runtime/debug.rs
1//! Debugging API.
2
3use crate::{
4 AnyRef, AsContext, AsContextMut, ExnRef, ExternRef, Func, Instance, Module, OwnedRooted,
5 StoreContext, StoreContextMut, Val,
6 store::{AutoAssertNoGc, StoreOpaque},
7 vm::{CurrentActivationBacktrace, VMContext},
8};
9use alloc::vec;
10use alloc::vec::Vec;
11use core::{ffi::c_void, ptr::NonNull};
12#[cfg(feature = "gc")]
13use wasmtime_environ::FrameTable;
14use wasmtime_environ::{
15 DefinedFuncIndex, FrameInstPos, FrameStackShape, FrameStateSlot, FrameStateSlotOffset,
16 FrameTableDescriptorIndex, FrameValType, FuncKey, Trap,
17};
18use wasmtime_unwinder::Frame;
19
20use super::store::AsStoreOpaque;
21
22impl<'a, T> StoreContextMut<'a, T> {
23 /// Provide an object that captures Wasm stack state, including
24 /// Wasm VM-level values (locals and operand stack).
25 ///
26 /// This object views all activations for the current store that
27 /// are on the stack. An activation is a contiguous sequence of
28 /// Wasm frames (called functions) that were called from host code
29 /// and called back out to host code. If there are activations
30 /// from multiple stores on the stack, for example if Wasm code in
31 /// one store calls out to host code which invokes another Wasm
32 /// function in another store, then the other stores are "opaque"
33 /// to our view here in the same way that host code is.
34 ///
35 /// Returns `None` if debug instrumentation is not enabled for
36 /// the engine containing this store.
37 pub fn debug_frames(self) -> Option<DebugFrameCursor<'a, T>> {
38 if !self.engine().tunables().debug_guest {
39 return None;
40 }
41
42 // SAFETY: This takes a mutable borrow of `self` (the
43 // `StoreOpaque`), which owns all active stacks in the
44 // store. We do not provide any API that could mutate the
45 // frames that we are walking on the `DebugFrameCursor`.
46 let iter = unsafe { CurrentActivationBacktrace::new(self) };
47 let mut view = DebugFrameCursor {
48 iter,
49 is_trapping_frame: false,
50 frames: vec![],
51 current: None,
52 };
53 view.move_to_parent(); // Load the first frame.
54 Some(view)
55 }
56}
57
58/// A view of an active stack frame, with the ability to move up the
59/// stack.
60///
61/// See the documentation on `Store::stack_value` for more information
62/// about which frames this view will show.
63pub struct DebugFrameCursor<'a, T: 'static> {
64 /// Iterator over frames.
65 ///
66 /// This iterator owns the store while the view exists (accessible
67 /// as `iter.store`).
68 iter: CurrentActivationBacktrace<'a, T>,
69
70 /// Is the next frame to be visited by the iterator a trapping
71 /// frame?
72 ///
73 /// This alters how we interpret `pc`: for a trap, we look at the
74 /// instruction that *starts* at `pc`, while for all frames
75 /// further up the stack (i.e., at a callsite), we look at the
76 /// instruction that *ends* at `pc`.
77 is_trapping_frame: bool,
78
79 /// Virtual frame queue: decoded from `iter`, not yet
80 /// yielded. Innermost frame on top (last).
81 ///
82 /// This is only non-empty when there is more than one virtual
83 /// frame in a physical frame (i.e., for inlining); thus, its size
84 /// is bounded by our inlining depth.
85 frames: Vec<VirtualFrame>,
86
87 /// Currently focused virtual frame.
88 current: Option<FrameData>,
89}
90
91impl<'a, T: 'static> DebugFrameCursor<'a, T> {
92 /// Move up to the next frame in the activation.
93 pub fn move_to_parent(&mut self) {
94 // If there are no virtual frames to yield, take and decode
95 // the next physical frame.
96 //
97 // Note that `if` rather than `while` here, and the assert
98 // that we get some virtual frames back, enforce the invariant
99 // that each physical frame decodes to at least one virtual
100 // frame (i.e., there are no physical frames for interstitial
101 // functions or other things that we completely ignore). If
102 // this ever changes, we can remove the assert and convert
103 // this to a loop that polls until it finds virtual frames.
104 self.current = None;
105 if self.frames.is_empty() {
106 let Some(next_frame) = self.iter.next() else {
107 return;
108 };
109 self.frames = VirtualFrame::decode(
110 self.iter.store.0.as_store_opaque(),
111 next_frame,
112 self.is_trapping_frame,
113 );
114 debug_assert!(!self.frames.is_empty());
115 self.is_trapping_frame = false;
116 }
117
118 // Take a frame and focus it as the current one.
119 self.current = self.frames.pop().map(|vf| FrameData::compute(vf));
120 }
121
122 /// Has the iterator reached the end of the activation?
123 pub fn done(&self) -> bool {
124 self.current.is_none()
125 }
126
127 fn frame_data(&self) -> &FrameData {
128 self.current.as_ref().expect("No current frame")
129 }
130
131 fn raw_instance(&self) -> &crate::vm::Instance {
132 // Read out the vmctx slot.
133
134 // SAFETY: vmctx is always at offset 0 in the slot.
135 // (See crates/cranelift/src/func_environ.rs in `update_stack_slot_vmctx()`.)
136 let vmctx: *mut VMContext = unsafe { *(self.frame_data().slot_addr as *mut _) };
137 let vmctx = NonNull::new(vmctx).expect("null vmctx in debug state slot");
138 // SAFETY: the stored vmctx value is a valid instance in this
139 // store; we only visit frames from this store in the
140 // backtrace.
141 let instance = unsafe { crate::vm::Instance::from_vmctx(vmctx) };
142 // SAFETY: the instance pointer read above is valid.
143 unsafe { instance.as_ref() }
144 }
145
146 /// Get the instance associated with the current frame.
147 pub fn instance(&mut self) -> Instance {
148 let instance = self.raw_instance();
149 Instance::from_wasmtime(instance.id(), self.iter.store.0.as_store_opaque())
150 }
151
152 /// Get the module associated with the current frame, if any
153 /// (i.e., not a container instance for a host-created entity).
154 pub fn module(&self) -> Option<&Module> {
155 let instance = self.raw_instance();
156 instance.runtime_module()
157 }
158
159 /// Get the raw function index associated with the current frame, and the
160 /// PC as an offset within its code section, if it is a Wasm
161 /// function directly from the given `Module` (rather than a
162 /// trampoline).
163 pub fn wasm_function_index_and_pc(&self) -> Option<(DefinedFuncIndex, u32)> {
164 let data = self.frame_data();
165 let FuncKey::DefinedWasmFunction(module, func) = data.func_key else {
166 return None;
167 };
168 debug_assert_eq!(
169 module,
170 self.module()
171 .expect("module should be defined if this is a defined function")
172 .env_module()
173 .module_index
174 );
175 Some((func, data.wasm_pc))
176 }
177
178 /// Get the number of locals in this frame.
179 pub fn num_locals(&self) -> u32 {
180 u32::try_from(self.frame_data().locals.len()).unwrap()
181 }
182
183 /// Get the depth of the operand stack in this frame.
184 pub fn num_stacks(&self) -> u32 {
185 u32::try_from(self.frame_data().stack.len()).unwrap()
186 }
187
188 /// Get the type and value of the given local in this frame.
189 ///
190 /// # Panics
191 ///
192 /// Panics if the index is out-of-range (greater than
193 /// `num_locals()`).
194 pub fn local(&mut self, index: u32) -> Val {
195 let data = self.frame_data();
196 let (offset, ty) = data.locals[usize::try_from(index).unwrap()];
197 let slot_addr = data.slot_addr;
198 // SAFETY: compiler produced metadata to describe this local
199 // slot and stored a value of the correct type into it.
200 unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
201 }
202
203 /// Get the type and value of the given operand-stack value in
204 /// this frame.
205 ///
206 /// Index 0 corresponds to the bottom-of-stack, and higher indices
207 /// from there are more recently pushed values. In other words,
208 /// index order reads the Wasm virtual machine's abstract stack
209 /// state left-to-right.
210 pub fn stack(&mut self, index: u32) -> Val {
211 let data = self.frame_data();
212 let (offset, ty) = data.stack[usize::try_from(index).unwrap()];
213 let slot_addr = data.slot_addr;
214 // SAFETY: compiler produced metadata to describe this
215 // operand-stack slot and stored a value of the correct type
216 // into it.
217 unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
218 }
219}
220
221/// Internal data pre-computed for one stack frame.
222///
223/// This combines physical frame info (pc, fp) with the module this PC
224/// maps to (yielding a frame table) and one frame as produced by the
225/// progpoint lookup (Wasm PC, frame descriptor index, stack shape).
226struct VirtualFrame {
227 /// The frame pointer.
228 fp: *const u8,
229 /// The resolved module handle for the physical PC.
230 ///
231 /// The module for each inlined frame within the physical frame is
232 /// resolved from the vmctx reachable for each such frame; this
233 /// module isused only for looking up the frame table.
234 module: Module,
235 /// The Wasm PC for this frame.
236 wasm_pc: u32,
237 /// The frame descriptor for this frame.
238 frame_descriptor: FrameTableDescriptorIndex,
239 /// The stack shape for this frame.
240 stack_shape: FrameStackShape,
241}
242
243impl VirtualFrame {
244 /// Return virtual frames corresponding to a physical frame, from
245 /// outermost to innermost.
246 fn decode(store: &mut StoreOpaque, frame: Frame, is_trapping_frame: bool) -> Vec<VirtualFrame> {
247 let (module_with_code, pc) = store
248 .modules()
249 .module_and_code_by_pc(frame.pc())
250 .expect("Wasm frame PC does not correspond to a module");
251 let module = module_with_code.module();
252 let table = module.frame_table().unwrap();
253 let pc = u32::try_from(pc).expect("PC offset too large");
254 let pos = if is_trapping_frame {
255 FrameInstPos::Pre
256 } else {
257 FrameInstPos::Post
258 };
259 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");
260
261 program_points
262 .map(|(wasm_pc, frame_descriptor, stack_shape)| VirtualFrame {
263 fp: core::ptr::with_exposed_provenance(frame.fp()),
264 module: module.clone(),
265 wasm_pc,
266 frame_descriptor,
267 stack_shape,
268 })
269 .collect()
270 }
271}
272
273/// Data computed when we visit a given frame.
274struct FrameData {
275 slot_addr: *const u8,
276 func_key: FuncKey,
277 wasm_pc: u32,
278 /// Shape of locals in this frame.
279 ///
280 /// We need to store this locally because `FrameView` cannot
281 /// borrow the store: it needs a mut borrow, and an iterator
282 /// cannot yield the same mut borrow multiple times because it
283 /// cannot control the lifetime of the values it yields (the
284 /// signature of `next()` does not bound the return value to the
285 /// `&mut self` arg).
286 locals: Vec<(FrameStateSlotOffset, FrameValType)>,
287 /// Shape of the stack slots at this program point in this frame.
288 ///
289 /// In addition to the borrowing-related reason above, we also
290 /// materialize this because we want to provide O(1) access to the
291 /// stack by depth, and the frame slot descriptor stores info in a
292 /// linked-list (actually DAG, with dedup'ing) way.
293 stack: Vec<(FrameStateSlotOffset, FrameValType)>,
294}
295
296impl FrameData {
297 fn compute(frame: VirtualFrame) -> Self {
298 let frame_table = frame.module.frame_table().unwrap();
299 // Parse the frame descriptor.
300 let (data, slot_to_fp_offset) = frame_table
301 .frame_descriptor(frame.frame_descriptor)
302 .unwrap();
303 let frame_state_slot = FrameStateSlot::parse(data).unwrap();
304 let slot_addr = frame
305 .fp
306 .wrapping_sub(usize::try_from(slot_to_fp_offset).unwrap());
307
308 // Materialize the stack shape so we have O(1) access to its
309 // elements, and so we don't need to keep the borrow to the
310 // module alive.
311 let mut stack = frame_state_slot
312 .stack(frame.stack_shape)
313 .collect::<Vec<_>>();
314 stack.reverse(); // Put top-of-stack last.
315
316 // Materialize the local offsets/types so we don't need to
317 // keep the borrow to the module alive.
318 let locals = frame_state_slot.locals().collect::<Vec<_>>();
319
320 FrameData {
321 slot_addr,
322 func_key: frame_state_slot.func_key(),
323 wasm_pc: frame.wasm_pc,
324 stack,
325 locals,
326 }
327 }
328}
329
330/// Read the value at the given offset.
331///
332/// # Safety
333///
334/// The `offset` and `ty` must correspond to a valid value written
335/// to the frame by generated code of the correct type. This will
336/// be the case if this information comes from the frame tables
337/// (as long as the frontend that generates the tables and
338/// instrumentation is correct, and as long as the tables are
339/// preserved through serialization).
340unsafe fn read_value(
341 store: &mut StoreOpaque,
342 slot_base: *const u8,
343 offset: FrameStateSlotOffset,
344 ty: FrameValType,
345) -> Val {
346 let address = unsafe { slot_base.offset(isize::try_from(offset.offset()).unwrap()) };
347
348 // SAFETY: each case reads a value from memory that should be
349 // valid according to our safety condition.
350 match ty {
351 FrameValType::I32 => {
352 let value = unsafe { *(address as *const i32) };
353 Val::I32(value)
354 }
355 FrameValType::I64 => {
356 let value = unsafe { *(address as *const i64) };
357 Val::I64(value)
358 }
359 FrameValType::F32 => {
360 let value = unsafe { *(address as *const u32) };
361 Val::F32(value)
362 }
363 FrameValType::F64 => {
364 let value = unsafe { *(address as *const u64) };
365 Val::F64(value)
366 }
367 FrameValType::V128 => {
368 let value = unsafe { *(address as *const u128) };
369 Val::V128(value.into())
370 }
371 FrameValType::AnyRef => {
372 let mut nogc = AutoAssertNoGc::new(store);
373 let value = unsafe { *(address as *const u32) };
374 let value = AnyRef::_from_raw(&mut nogc, value);
375 Val::AnyRef(value)
376 }
377 FrameValType::ExnRef => {
378 let mut nogc = AutoAssertNoGc::new(store);
379 let value = unsafe { *(address as *const u32) };
380 let value = ExnRef::_from_raw(&mut nogc, value);
381 Val::ExnRef(value)
382 }
383 FrameValType::ExternRef => {
384 let mut nogc = AutoAssertNoGc::new(store);
385 let value = unsafe { *(address as *const u32) };
386 let value = ExternRef::_from_raw(&mut nogc, value);
387 Val::ExternRef(value)
388 }
389 FrameValType::FuncRef => {
390 let value = unsafe { *(address as *const *mut c_void) };
391 let value = unsafe { Func::_from_raw(store, value) };
392 Val::FuncRef(value)
393 }
394 FrameValType::ContRef => {
395 unimplemented!("contref values are not implemented in the host API yet")
396 }
397 }
398}
399
400/// Compute raw pointers to all GC refs in the given frame.
401// Note: ideally this would be an impl Iterator, but this is quite
402// awkward because of the locally computed data (FrameStateSlot::parse
403// structured result) within the closure borrowed by a nested closure.
404#[cfg(feature = "gc")]
405pub(crate) fn gc_refs_in_frame<'a>(ft: FrameTable<'a>, pc: u32, fp: *mut usize) -> Vec<*mut u32> {
406 let fp = fp.cast::<u8>();
407 let mut ret = vec![];
408 if let Some(frames) = ft.find_program_point(pc, FrameInstPos::Post) {
409 for (_wasm_pc, frame_desc, stack_shape) in frames {
410 let (frame_desc_data, slot_to_fp_offset) = ft.frame_descriptor(frame_desc).unwrap();
411 let frame_base = unsafe { fp.offset(-isize::try_from(slot_to_fp_offset).unwrap()) };
412 let frame_desc = FrameStateSlot::parse(frame_desc_data).unwrap();
413 for (offset, ty) in frame_desc.stack_and_locals(stack_shape) {
414 match ty {
415 FrameValType::AnyRef | FrameValType::ExnRef | FrameValType::ExternRef => {
416 let slot = unsafe {
417 frame_base
418 .offset(isize::try_from(offset.offset()).unwrap())
419 .cast::<u32>()
420 };
421 ret.push(slot);
422 }
423 FrameValType::ContRef | FrameValType::FuncRef => {}
424 FrameValType::I32
425 | FrameValType::I64
426 | FrameValType::F32
427 | FrameValType::F64
428 | FrameValType::V128 => {}
429 }
430 }
431 }
432 }
433 ret
434}
435
436impl<'a, T: 'static> AsContext for DebugFrameCursor<'a, T> {
437 type Data = T;
438 fn as_context(&self) -> StoreContext<'_, Self::Data> {
439 StoreContext(self.iter.store.0)
440 }
441}
442impl<'a, T: 'static> AsContextMut for DebugFrameCursor<'a, T> {
443 fn as_context_mut(&mut self) -> StoreContextMut<'_, Self::Data> {
444 StoreContextMut(self.iter.store.0)
445 }
446}
447
448/// One debug event that occurs when running Wasm code on a store with
449/// a debug handler attached.
450#[derive(Debug)]
451pub enum DebugEvent<'a> {
452 /// An `anyhow::Error` was raised by a hostcall.
453 HostcallError(&'a anyhow::Error),
454 /// An exception is thrown and caught by Wasm. The current state
455 /// is at the throw-point.
456 CaughtExceptionThrown(OwnedRooted<ExnRef>),
457 /// An exception was not caught and is escaping to the host.
458 UncaughtExceptionThrown(OwnedRooted<ExnRef>),
459 /// A Wasm trap occurred.
460 Trap(Trap),
461}
462
463/// A handler for debug events.
464///
465/// This is an async callback that is invoked directly within the
466/// context of a debug event that occurs, i.e., with the Wasm code
467/// still on the stack. The callback can thus observe that stack, up
468/// to the most recent entry to Wasm.[^1]
469///
470/// Because this callback receives a `StoreContextMut`, it has full
471/// access to any state that any other hostcall has, including the
472/// `T`. In that way, it is like an epoch-deadline callback or a
473/// call-hook callback. It also "freezes" the entire store for the
474/// duration of the debugger callback future.
475///
476/// In the future, we expect to provide an "externally async" API on
477/// the `Store` that allows receiving a stream of debug events and
478/// accessing the store mutably while frozen; that will need to
479/// integrate with [`Store::run_concurrent`] to properly timeslice and
480/// scope the mutable access to the store, and has not been built
481/// yet. In the meantime, it should be possible to build a fully
482/// functional debugger with this async-callback API by channeling
483/// debug events out, and requests to read the store back in, over
484/// message-passing channels between the callback and an external
485/// debugger main loop.
486///
487/// Note that the `handle` hook may use its mutable store access to
488/// invoke another Wasm. Debug events will also be caught and will
489/// cause further `handle` invocations during this recursive
490/// invocation. It is up to the debugger to handle any implications of
491/// this reentrancy (e.g., implications on a duplex channel protocol
492/// with an event/continue handshake) if it does so.
493///
494/// Note also that this trait has `Clone` as a supertrait, and the
495/// handler is cloned at every invocation as an artifact of the
496/// internal ownership structure of Wasmtime: the handler itself is
497/// owned by the store, but also receives a mutable borrow to the
498/// whole store, so we need to clone it out to invoke it. It is
499/// recommended that this trait be implemented by a type that is cheap
500/// to clone: for example, a single `Arc` handle to debugger state.
501///
502/// [^1]: Providing visibility further than the most recent entry to
503/// Wasm is not directly possible because it could see into
504/// another async stack, and the stack that polls the future
505/// running a particular Wasm invocation could change after each
506/// suspend point in the handler.
507pub trait DebugHandler: Clone + Send + Sync + 'static {
508 /// The data expected on the store that this handler is attached
509 /// to.
510 type Data;
511
512 /// Handle a debug event.
513 fn handle(
514 &self,
515 store: StoreContextMut<'_, Self::Data>,
516 event: DebugEvent<'_>,
517 ) -> impl Future<Output = ()> + Send;
518}