//! Wasmtime debugger functionality. //! //! This crate builds on top of the core Wasmtime crate's //! guest-debugger APIs to present an environment where a debugger //! runs as a "co-running process" and sees the debuggee as a a //! provider of a stream of events, on which actions can be taken //! between each event. //! //! In the future, this crate will also provide a WIT-level API and //! world in which to run debugger components. use std::{ any::Any, future::Future, pin::Pin, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, }; use tokio::{ sync::{Mutex, mpsc}, task::JoinHandle, }; use wasmtime::{ AsContextMut, DebugEvent, DebugHandler, Engine, ExnRef, OwnedRooted, Result, Store, StoreContextMut, Trap, }; mod host; pub use host::{DebuggerComponent, add_debuggee, add_to_linker, wit}; /// A `Debuggee` wraps up state associated with debugging the code /// running in a single `Store`. /// /// It acts as a Future combinator, wrapping an inner async body that /// performs some actions on a store. Those actions are subject to the /// debugger, and debugger events will be raised as appropriate. From /// the "outside" of this combinator, it is always in one of two /// states: running or paused. When paused, it acts as a /// `StoreContextMut` and can allow examining the paused execution's /// state. One runs until the next event suspends execution by /// invoking `Debuggee::run`. pub struct Debuggee { /// A handle to the Engine that the debuggee store lives within. engine: Engine, /// State: either a task handle or the store when passed out of /// the complete task. state: DebuggeeState, /// The store, once complete. store: Option>, in_tx: mpsc::Sender>, out_rx: mpsc::Receiver>, handle: Option>>, /// Flag shared with the inner handler: set to `true` by /// `interrupt()` so the next epoch yield is surfaced as an /// `Interrupted` event rather than eaten by the handler. Epoch /// yields serve two purposes, namely ensuring regular yields to /// the event loop and enacting an explicit interrupt, and this /// flag distinguishes those cases. interrupt_pending: Arc, } /// State machine from the perspective of the outer logic. /// /// The intermediate states here, and the separation of these states /// from the `JoinHandle` above, are what allow us to implement a /// cancel-safe version of `Debuggee::run` below. /// /// The state diagram for the outer logic is: /// /// ```plain /// (start) /// v /// | /// .--->---------. v /// | .----< Paused <-----------------------------------------------. /// | | v | /// | | | (async fn run() starts, sends Command::Continue) | /// | | | | /// | | v ^ /// | | Running | /// | | v v (async fn run() receives Response::Paused, returns) | /// | | | |_____________________________________________________| /// | | | /// | | | (async fn run() receives Response::Finished, returns) /// | | v /// | | Complete /// | | /// ^ | (async fn with_store() starts, sends Command::Query) /// | v /// | Queried /// | | /// | | (async fn with_store() receives Response::QueryResponse, returns) /// `---<-' /// ``` #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum DebuggeeState { /// Inner body has just been started. Initial, /// Inner body is running in an async task and not in a debugger /// callback. Outer logic is waiting for a `Response::Paused` or /// `Response::Complete`. Running, /// Inner body is running in an async task and at a debugger /// callback (or in the initial trampoline waiting for the first /// `Continue`). `Response::Paused` has been received. Outer /// logic has not sent any commands. Paused, /// We have sent a command to the inner body and are waiting for a /// response. Queried, /// Inner body is complete (has sent `Response::Finished` and we /// have received it). We may or may not have joined yet; if so, /// the `Option>` will be `None`. Complete, } /// Message from "outside" to the debug hook. /// /// The `Query` catch-all with a boxed closure is a little janky, but /// is the way that we provide access /// from outside to the Store (which is owned by `inner` above) /// only during pauses. Note that the future cannot take full /// ownership or a mutable borrow of the Store, because it cannot /// hold this across async yield points. /// /// Instead, the debugger body sends boxed closures which take the /// Store as a parameter (lifetime-limited not to escape that /// closure) out to this crate's implementation that runs inside of /// debugger-instrumentation callbacks (which have access to the /// Store during their duration). We send return values /// back. Return values are boxed Any values. /// /// If we wanted to make this a little more principled, we could /// come up with a Command/Response pair of enums for all possible /// closures and make everything more statically typed and less /// Box'd, but that would severely restrict the flexibility of the /// abstraction here and essentially require writing a full proxy /// of the debugger API. /// /// Furthermore, we expect to rip this out eventually when we move /// the debugger over to an async implementation based on /// `run_concurrent` and `Accessor`s (see #11896). Building things /// this way now will actually allow a less painful transition at /// that time, because we will have a bunch of closures accessing /// the store already and we can run those "with an accessor" /// instead. enum Command { Continue, Query(Box) -> Box + Send>), } enum Response { Paused(DebugRunResult), QueryResponse(Box), Finished(Store), } struct HandlerInner { in_rx: Mutex>>, out_tx: mpsc::Sender>, interrupt_pending: Arc, } struct Handler(Arc>); impl std::clone::Clone for Handler { fn clone(&self) -> Self { Handler(self.0.clone()) } } impl DebugHandler for Handler { type Data = T; async fn handle(&self, mut store: StoreContextMut<'_, T>, event: DebugEvent<'_>) { let mut in_rx = self.0.in_rx.lock().await; let result = match event { DebugEvent::HostcallError(_) => DebugRunResult::HostcallError, DebugEvent::CaughtExceptionThrown(exn) => DebugRunResult::CaughtExceptionThrown(exn), DebugEvent::UncaughtExceptionThrown(exn) => { DebugRunResult::UncaughtExceptionThrown(exn) } DebugEvent::Trap(trap) => DebugRunResult::Trap(trap), DebugEvent::Breakpoint => DebugRunResult::Breakpoint, DebugEvent::EpochYield => { // Only pause on epoch yields that were requested via // interrupt(). Other epoch ticks simply yield to the // event loop (functionality already implemented in // core Wasmtime; no need to do that yield here in the // debug handler). if !self.0.interrupt_pending.swap(false, Ordering::SeqCst) { return; } DebugRunResult::EpochYield } }; if self.0.out_tx.send(Response::Paused(result)).await.is_err() { // Outer Debuggee has been dropped: just continue // executing. return; } while let Some(cmd) = in_rx.recv().await { match cmd { Command::Query(closure) => { let result = closure(store.as_context_mut()); if self .0 .out_tx .send(Response::QueryResponse(result)) .await .is_err() { // Outer Debuggee has been dropped: just // continue executing. return; } } Command::Continue => { break; } } } } } impl Debuggee { /// Create a new Debugger that attaches to the given Store and /// runs the given inner body. /// /// The debugger is always in one of two states: running or /// paused. /// /// When paused, the holder of this object can invoke /// `Debuggee::run` to enter the running state. The inner body /// will run until paused by a debug event. While running, the /// future returned by either of these methods owns the `Debuggee` /// and hence no other methods can be invoked. /// /// When paused, the holder of this object can access the `Store` /// indirectly by providing a closure pub fn new(mut store: Store, inner: F) -> Debuggee where F: for<'a> FnOnce( &'a mut Store, ) -> Pin> + Send + 'a>> + Send + 'static, { let engine = store.engine().clone(); let (in_tx, in_rx) = mpsc::channel(1); let (out_tx, out_rx) = mpsc::channel(1); let interrupt_pending = Arc::new(AtomicBool::new(false)); let handle = tokio::spawn({ let interrupt_pending = interrupt_pending.clone(); async move { // Create the handler that's invoked from within the async // debug-event callback. let out_tx_clone = out_tx.clone(); let handler = Handler(Arc::new(HandlerInner { in_rx: Mutex::new(in_rx), out_tx, interrupt_pending, })); // Emulate a breakpoint at startup. log::trace!("inner debuggee task: first breakpoint"); handler .handle(store.as_context_mut(), DebugEvent::Breakpoint) .await; log::trace!("inner debuggee task: first breakpoint resumed"); // Now invoke the actual inner body. store.set_debug_handler(handler); log::trace!("inner debuggee task: running `inner`"); let result = inner(&mut store).await; log::trace!("inner debuggee task: done with `inner`"); let _ = out_tx_clone.send(Response::Finished(store)).await; result } }); Debuggee { engine, state: DebuggeeState::Initial, store: None, in_tx, out_rx, interrupt_pending, handle: Some(handle), } } /// Is the inner body done running? pub fn is_complete(&self) -> bool { match self.state { DebuggeeState::Complete => true, _ => false, } } /// Get the Engine associated with the debuggee. pub fn engine(&self) -> &Engine { &self.engine } /// Get the interrupt-pending flag. Setting this to `true` causes /// the next epoch yield to surface as an `Interrupted` event. pub fn interrupt_pending(&self) -> &Arc { &self.interrupt_pending } async fn wait_for_initial(&mut self) -> Result<()> { if let DebuggeeState::Initial = &self.state { // Need to receive and discard first `Paused`. let response = self .out_rx .recv() .await .ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?; assert!(matches!(response, Response::Paused(_))); self.state = DebuggeeState::Paused; } Ok(()) } /// Run the inner body until the next debug event. /// /// This method is cancel-safe, and no events will be lost. pub async fn run(&mut self) -> Result { log::trace!("running: state is {:?}", self.state); self.wait_for_initial().await?; match self.state { DebuggeeState::Initial => unreachable!(), DebuggeeState::Paused => { log::trace!("sending Continue"); self.in_tx .send(Command::Continue) .await .map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?; log::trace!("sent Continue"); // If that `send` was canceled, the command was not // sent, so it's fine to remain in `Paused`. If it // succeeded and we reached here, transition to // `Running` so we don't re-send. self.state = DebuggeeState::Running; } DebuggeeState::Running => { // Previous `run()` must have been canceled; no action // to take here. } DebuggeeState::Queried => { // We expect to receive a `QueryResponse`; drop it if // the query was canceled, then transition back to // `Paused`. log::trace!("in Queried; receiving"); let response = self.out_rx.recv().await.ok_or_else(|| { wasmtime::format_err!("Premature close of debugger channel") })?; log::trace!("in Queried; received, dropping"); assert!(matches!(response, Response::QueryResponse(_))); self.state = DebuggeeState::Paused; // Now send a `Continue`, as above. log::trace!("in Paused; sending Continue"); self.in_tx .send(Command::Continue) .await .map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?; self.state = DebuggeeState::Running; } DebuggeeState::Complete => { panic!("Cannot `run()` an already-complete Debuggee"); } } // At this point, the inner task is in Running state. We // expect to receive a message when it next pauses or // completes. If this `recv()` is canceled, no message is // lost, and the state above accurately reflects what must be // done on the next `run()`. log::trace!("waiting for response"); let response = self .out_rx .recv() .await .ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?; match response { Response::Finished(store) => { log::trace!("got Finished"); self.state = DebuggeeState::Complete; self.store = Some(store); Ok(DebugRunResult::Finished) } Response::Paused(result) => { log::trace!("got Paused"); self.state = DebuggeeState::Paused; Ok(result) } Response::QueryResponse(_) => { panic!("Invalid debug response"); } } } /// Run the debugger body until completion, with no further events. pub async fn finish(&mut self) -> Result<()> { if self.is_complete() { return Ok(()); } loop { match self.run().await? { DebugRunResult::Finished => break, e => { log::trace!("finish: event {e:?}"); } } } if let Some(handle) = self.handle.take() { handle.await??; } assert!(self.is_complete()); Ok(()) } /// Perform some action on the contained `Store` while not running. /// /// This may only be invoked before the inner body finishes and /// when it is paused; that is, when the `Debuggee` is initially /// created and after any call to `run()` returns a result other /// than `DebugRunResult::Finished`. If an earlier `run()` /// invocation was canceled, it must be re-invoked and return /// successfully before a query is made. /// /// This is cancel-safe; if canceled, the result of the query will /// be dropped. pub async fn with_store< F: FnOnce(StoreContextMut<'_, T>) -> R + Send + 'static, R: Send + 'static, >( &mut self, f: F, ) -> Result { if let Some(store) = self.store.as_mut() { return Ok(f(store.as_context_mut())); } self.wait_for_initial().await?; match self.state { DebuggeeState::Initial => unreachable!(), DebuggeeState::Queried => { // Earlier query canceled; drop its response first. let response = self.out_rx.recv().await.ok_or_else(|| { wasmtime::format_err!("Premature close of debugger channel") })?; assert!(matches!(response, Response::QueryResponse(_))); self.state = DebuggeeState::Paused; } DebuggeeState::Running => { // Results from a canceled `run()`; `run()` must // complete before this can be invoked. panic!("Cannot query in Running state"); } DebuggeeState::Complete => { panic!("Cannot query when complete"); } DebuggeeState::Paused => { // OK -- this is the state we want. } } log::trace!("sending query in with_store"); self.in_tx .send(Command::Query(Box::new(|store| Box::new(f(store))))) .await .map_err(|_| wasmtime::format_err!("Premature close of debugger channel"))?; self.state = DebuggeeState::Queried; let response = self .out_rx .recv() .await .ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?; let Response::QueryResponse(resp) = response else { wasmtime::bail!("Incorrect response from debugger task"); }; self.state = DebuggeeState::Paused; Ok(*resp.downcast::().expect("type mismatch")) } } /// The result of one call to `Debuggee::run()`. /// /// This is similar to `DebugEvent` but without the lifetime, so it /// can be sent across async tasks, and incorporates the possibility /// of completion (`Finished`) as well. #[derive(Debug)] pub enum DebugRunResult { /// Execution of the inner body finished. Finished, /// An error was raised by a hostcall. HostcallError, /// Wasm execution was interrupted by an epoch change. EpochYield, /// An exception is thrown and caught by Wasm. The current state /// is at the throw-point. CaughtExceptionThrown(OwnedRooted), /// An exception was not caught and is escaping to the host. UncaughtExceptionThrown(OwnedRooted), /// A Wasm trap occurred. Trap(Trap), /// A breakpoint was reached. Breakpoint, } #[cfg(test)] mod test { use super::*; use wasmtime::*; #[tokio::test] #[cfg_attr(miri, ignore)] async fn basic_debugger() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let mut config = Config::new(); config.guest_debug(true); let engine = Engine::new(&config)?; let module = Module::new( &engine, r#" (module (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; let mut store = Store::new(&engine, ()); let instance = Instance::new_async(&mut store, &module, &[]).await?; let main = instance.get_func(&mut store, "main").unwrap(); let mut debuggee = Debuggee::new(store, move |store| { Box::pin(async move { let mut results = [Val::I32(0)]; store.edit_breakpoints().unwrap().single_step(true).unwrap(); main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..]) .await?; assert_eq!(results[0].unwrap_i32(), 3); main.call_async(&mut *store, &[Val::I32(3), Val::I32(4)], &mut results[..]) .await?; assert_eq!(results[0].unwrap_i32(), 7); Ok(()) }) }); let event = debuggee.run().await?; assert!(matches!(event, DebugRunResult::Breakpoint)); // At (before executing) first `local.get`. debuggee .with_store(|mut store| { let frame = store.debug_exit_frames().next().unwrap(); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .0 .as_u32(), 0 ); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .1 .raw(), 36 ); assert_eq!(frame.num_locals(&mut store).unwrap(), 2); assert_eq!(frame.num_stacks(&mut store).unwrap(), 0); assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2); let frame = frame.parent(&mut store).unwrap(); assert!(frame.is_none()); }) .await?; let event = debuggee.run().await?; // At second `local.get`. assert!(matches!(event, DebugRunResult::Breakpoint)); debuggee .with_store(|mut store| { let frame = store.debug_exit_frames().next().unwrap(); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .0 .as_u32(), 0 ); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .1 .raw(), 38 ); assert_eq!(frame.num_locals(&mut store).unwrap(), 2); assert_eq!(frame.num_stacks(&mut store).unwrap(), 1); assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2); assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 1); let frame = frame.parent(&mut store).unwrap(); assert!(frame.is_none()); }) .await?; let event = debuggee.run().await?; // At `i32.add`. assert!(matches!(event, DebugRunResult::Breakpoint)); debuggee .with_store(|mut store| { let frame = store.debug_exit_frames().next().unwrap(); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .0 .as_u32(), 0 ); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .1 .raw(), 40 ); assert_eq!(frame.num_locals(&mut store).unwrap(), 2); assert_eq!(frame.num_stacks(&mut store).unwrap(), 2); assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2); assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(frame.stack(&mut store, 1).unwrap().unwrap_i32(), 2); let frame = frame.parent(&mut store).unwrap(); assert!(frame.is_none()); }) .await?; let event = debuggee.run().await?; // At return point. assert!(matches!(event, DebugRunResult::Breakpoint)); debuggee .with_store(|mut store| { let frame = store.debug_exit_frames().next().unwrap(); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .0 .as_u32(), 0 ); assert_eq!( frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap() .1 .raw(), 41 ); assert_eq!(frame.num_locals(&mut store).unwrap(), 2); assert_eq!(frame.num_stacks(&mut store).unwrap(), 1); assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2); assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 3); let frame = frame.parent(&mut store).unwrap(); assert!(frame.is_none()); }) .await?; // Now disable breakpoints before continuing. Second call should proceed with no more events. debuggee .with_store(|store| { store .edit_breakpoints() .unwrap() .single_step(false) .unwrap(); }) .await?; let event = debuggee.run().await?; assert!(matches!(event, DebugRunResult::Finished)); assert!(debuggee.is_complete()); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn early_finish() -> Result<()> { let _ = env_logger::try_init(); let mut config = Config::new(); config.guest_debug(true); let engine = Engine::new(&config)?; let module = Module::new( &engine, r#" (module (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; let mut store = Store::new(&engine, ()); let instance = Instance::new_async(&mut store, &module, &[]).await?; let main = instance.get_func(&mut store, "main").unwrap(); let mut debuggee = Debuggee::new(store, move |store| { Box::pin(async move { let mut results = [Val::I32(0)]; store.edit_breakpoints().unwrap().single_step(true).unwrap(); main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..]) .await?; assert_eq!(results[0].unwrap_i32(), 3); Ok(()) }) }); debuggee.finish().await?; assert!(debuggee.is_complete()); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn drop_debuggee_and_store() -> Result<()> { let _ = env_logger::try_init(); let mut config = Config::new(); config.guest_debug(true); let engine = Engine::new(&config)?; let module = Module::new( &engine, r#" (module (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; let mut store = Store::new(&engine, ()); let instance = Instance::new_async(&mut store, &module, &[]).await?; let main = instance.get_func(&mut store, "main").unwrap(); let mut debuggee = Debuggee::new(store, move |store| { Box::pin(async move { let mut results = [Val::I32(0)]; store.edit_breakpoints().unwrap().single_step(true).unwrap(); main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..]) .await?; assert_eq!(results[0].unwrap_i32(), 3); Ok(()) }) }); // Step once, then drop everything at the end of this // function. Wasmtime's fiber cleanup should safely happen // without attempting to raise debug async handler calls with // missing async context. let _ = debuggee.run().await?; Ok(()) } }