//! Tests for instrumentation-based debugging. use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use wasmtime::{ AsContextMut, Caller, Config, DebugEvent, DebugHandler, Engine, Extern, FrameHandle, Func, Global, GlobalType, Instance, Module, ModulePC, Mutability, Store, StoreContextMut, Val, ValType, }; use crate::async_functions::PollOnce; #[test] fn debugging_does_not_work_with_signal_based_traps() { let mut config = Config::default(); config.guest_debug(true).signals_based_traps(true); let err = Engine::new(&config).expect_err("invalid config should produce an error"); assert!(format!("{err:?}").contains("cannot use signals-based traps")); } #[test] fn debugging_apis_are_denied_without_debugging() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(false); let engine = Engine::new(&config)?; let module = Module::new(&engine, "(module (global $g (mut i32) (i32.const 0)))")?; let mut store = Store::new(&engine, ()); let instance = Instance::new(&mut store, &module, &[])?; assert!(store.debug_exit_frames().next().is_none()); assert!(instance.debug_global(&mut store, 0).is_none()); Ok(()) } fn get_module_and_store( c: C, wat: &str, ) -> wasmtime::Result<(Module, Store<()>)> { let mut config = Config::default(); config.guest_debug(true); config.wasm_exceptions(true); c(&mut config); let engine = Engine::new(&config)?; let module = Module::new(&engine, wat)?; Ok((module, Store::new(&engine, ()))) } fn test_stack_values< C: Fn(&mut Config), F: Fn(Caller<'_, ()>) -> wasmtime::Result<()> + Send + Sync + 'static, >( wat: &str, c: C, f: F, ) -> wasmtime::Result<()> { let (module, mut store) = get_module_and_store(c, wat)?; let func = Func::wrap(&mut store, move |caller: Caller<'_, ()>| { f(caller)?; Ok(()) }); let instance = Instance::new(&mut store, &module, &[Extern::Func(func)])?; let mut results = []; instance .get_func(&mut store, "main") .unwrap() .call(&mut store, &[], &mut results)?; Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn stack_values_two_frames() -> wasmtime::Result<()> { let _ = env_logger::try_init(); for inlining in [false, true] { test_stack_values( r#" (module (import "" "host" (func)) (func (export "main") i32.const 1 i32.const 2 call 2 drop) (func (param i32 i32) (result i32) local.get 0 local.get 1 call 0 i32.add)) "#, |config| { config.compiler_inlining(inlining); if inlining { unsafe { config.cranelift_flag_set("wasmtime_inlining_intra_module", "true"); } } }, |mut caller: Caller<'_, ()>| { let stack = caller.debug_exit_frames().next().unwrap(); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .0 .as_u32(), 1 ); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .1 .raw(), 67 ); assert_eq!(stack.num_locals(&mut caller)?, 2); assert_eq!(stack.num_stacks(&mut caller)?, 2); assert_eq!(stack.local(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(stack.local(&mut caller, 1)?.unwrap_i32(), 2); assert_eq!(stack.stack(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(stack.stack(&mut caller, 1)?.unwrap_i32(), 2); let stack = stack.parent(&mut caller)?.unwrap(); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .0 .as_u32(), 0 ); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .1 .raw(), 57 ); let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); Ok(()) }, )?; } Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn stack_values_exceptions() -> wasmtime::Result<()> { test_stack_values( r#" (module (tag $t (param i32)) (import "" "host" (func)) (func (export "main") (block $b (result i32) (try_table (catch $t $b) (throw $t (i32.const 42))) i32.const 0) (call 0) (drop))) "#, |_config| {}, |mut caller: Caller<'_, ()>| { let stack = caller.debug_exit_frames().next().unwrap(); assert_eq!(stack.num_stacks(&mut caller)?, 1); assert_eq!(stack.stack(&mut caller, 0)?.unwrap_i32(), 42); let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); Ok(()) }, ) } #[test] #[cfg_attr(miri, ignore)] fn stack_values_dead_gc_ref() -> wasmtime::Result<()> { test_stack_values( r#" (module (type $s (struct)) (import "" "host" (func)) (func (export "main") (struct.new $s) (call 0) (drop))) "#, |config| { config.wasm_gc(true); }, |mut caller: Caller<'_, ()>| { let stack = caller.debug_exit_frames().next().unwrap(); assert_eq!(stack.num_stacks(&mut caller)?, 1); assert!(stack.stack(&mut caller, 0)?.unwrap_anyref().is_some()); let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); Ok(()) }, ) } #[test] #[cfg_attr(miri, ignore)] fn gc_access_during_call() -> wasmtime::Result<()> { test_stack_values( r#" (module (type $s (struct (field i32))) (import "" "host" (func)) (func (export "main") (local $l (ref null $s)) (local.set $l (struct.new $s (i32.const 42))) (call 0))) "#, |config| { config.wasm_gc(true); }, |mut caller: Caller<'_, ()>| { let stack = caller.debug_exit_frames().next().unwrap(); // Do a GC while we hold the stack cursor. caller.as_context_mut().gc(None).unwrap(); assert_eq!(stack.num_stacks(&mut caller)?, 0); assert_eq!(stack.num_locals(&mut caller)?, 1); // Note that this struct is dead during the call, and the // ref could otherwise be optimized away (no longer in the // stackmap at this point); but we verify it is still // alive here because it is rooted in the // debug-instrumentation slot. let s = stack .local(&mut caller, 0)? .unwrap_any_ref() .unwrap() .unwrap_struct(&caller) .unwrap(); assert_eq!(s.field(&mut caller, 0).unwrap().unwrap_i32(), 42); let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); Ok(()) }, ) } #[test] #[cfg_attr(miri, ignore)] fn stack_values_two_activations() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let mut config = Config::default(); config.guest_debug(true); config.wasm_exceptions(true); let engine = Engine::new(&config)?; let module1 = Module::new( &engine, r#" (module (import "" "host1" (func (param i32 i32) (result i32))) (func (export "main") (result i32) i32.const 1 i32.const 2 call 0)) "#, )?; let module2 = Module::new( &engine, r#" (module (import "" "host2" (func)) (func (export "inner") (param i32 i32) (result i32) local.get 0 local.get 1 call 0 i32.add)) "#, )?; let mut store = Store::new(&engine, ()); let module1_clone = module1.clone(); let module2_clone = module2.clone(); let host2 = Func::wrap(&mut store, move |mut caller: Caller<'_, ()>| { let exits = caller.debug_exit_frames().collect::>(); assert_eq!(exits.len(), 2); let stack = exits[0].clone(); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .0 .as_u32(), 0 ); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .1 .raw(), 58 ); assert!(Module::same( stack.module(&mut caller)?.unwrap(), &module2_clone )); assert_eq!(stack.num_locals(&mut caller)?, 2); assert_eq!(stack.num_stacks(&mut caller)?, 2); assert_eq!(stack.local(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(stack.local(&mut caller, 1)?.unwrap_i32(), 2); assert_eq!(stack.stack(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(stack.stack(&mut caller, 1)?.unwrap_i32(), 2); let inner_instance = stack.instance(&mut caller)?; let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); let stack = exits[1].clone(); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .0 .as_u32(), 0 ); assert_eq!( stack .wasm_function_index_and_pc(&mut caller)? .unwrap() .1 .raw(), 58 ); assert!(Module::same( stack.module(&mut caller)?.unwrap(), &module1_clone )); assert_eq!(stack.num_locals(&mut caller)?, 0); assert_eq!(stack.num_stacks(&mut caller)?, 2); assert_eq!(stack.stack(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(stack.stack(&mut caller, 1)?.unwrap_i32(), 2); let outer_instance = stack.instance(&mut caller)?; assert_ne!(inner_instance, outer_instance); let stack = stack.parent(&mut caller)?; assert!(stack.is_none()); Ok(()) }); let instance2 = Instance::new(&mut store, &module2, &[Extern::Func(host2)])?; let inner = instance2.get_func(&mut store, "inner").unwrap(); let host1 = Func::wrap( &mut store, move |mut caller: Caller<'_, ()>, a: i32, b: i32| -> i32 { let mut results = [Val::I32(0)]; inner .call(&mut caller, &[Val::I32(a), Val::I32(b)], &mut results[..]) .unwrap(); results[0].unwrap_i32() }, ); let instance1 = Instance::new(&mut store, &module1, &[Extern::Func(host1)])?; let main = instance1.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; main.call(&mut store, &[], &mut results)?; assert_eq!(results[0].unwrap_i32(), 3); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn debug_frames_on_store_with_no_wasm_activation() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ()); let frames = store.debug_exit_frames().collect::>(); assert_eq!(frames.len(), 0); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn private_entity_access() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(true); config.wasm_gc(true); config.gc_support(true); config.wasm_exceptions(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ()); let module = Module::new( &engine, r#" (module (import "" "i" (global (mut i32))) (import "" "f" (func (result i32))) (global $g (mut i32) (i32.const 0)) (memory $m 1 1) (table $t 10 10 i31ref) (tag $tag (param f64)) (func (export "main") ;; $g := 42 i32.const 42 global.set $g ;; $m[1024] := 1 i32.const 1024 i32.const 1 i32.store8 $m ;; $t[1] := (ref.i31 (i32.const 100)) i32.const 1 i32.const 100 ref.i31 table.set $t) (func (param i32) local.get 0 global.set $g)) "#, )?; let host_global = Global::new( &mut store, GlobalType::new(ValType::I32, Mutability::Var), Val::I32(1000), )?; let host_func = Func::wrap(&mut store, |_caller: Caller<'_, ()>| -> i32 { 7 }); let instance = Instance::new( &mut store, &module, &[Extern::Global(host_global), Extern::Func(host_func)], )?; let func = instance.get_func(&mut store, "main").unwrap(); func.call(&mut store, &[], &mut [])?; // Nothing is exported except for `main`, yet we can still access // (below). let exports = instance.exports(&mut store).collect::>(); assert_eq!(exports.len(), 1); assert!(exports.into_iter().next().unwrap().into_func().is_some()); // We can call a non-exported function. let f = instance.debug_function(&mut store, 2).unwrap(); f.call(&mut store, &[Val::I32(1234)], &mut [])?; let g = instance.debug_global(&mut store, 1).unwrap(); assert_eq!(g.get(&mut store).unwrap_i32(), 1234); let m = instance.debug_memory(&mut store, 0).unwrap(); assert_eq!(m.data(&mut store)[1024], 1); let t = instance.debug_table(&mut store, 0).unwrap(); let t_val = t.get(&mut store, 1).unwrap(); let t_val = t_val.as_any().unwrap().unwrap().unwrap_i31(&store).unwrap(); assert_eq!(t_val.get_u32(), 100); let tag = instance.debug_tag(&mut store, 0).unwrap(); assert!(matches!( tag.ty(&store).ty().param(0).unwrap(), ValType::F64 )); // Check that we can access an imported global in the instance's // index space. let host_global_import = instance.debug_global(&mut store, 0).unwrap(); assert_eq!(host_global_import.get(&mut store).unwrap_i32(), 1000); // Check that we can call an imported function in the instance's // index space. let host_func_import = instance.debug_function(&mut store, 0).unwrap(); let mut results = [Val::I32(0)]; host_func_import.call(&mut store, &[], &mut results[..])?; assert_eq!(results[0].unwrap_i32(), 7); // Check that out-of-bounds returns `None` rather than panic'ing. assert!(instance.debug_global(&mut store, 2).is_none()); Ok(()) } #[test] #[cfg_attr(miri, ignore)] #[cfg(target_pointer_width = "64")] // Threads not supported on 32-bit systems. fn private_entity_access_shared_memory() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(true); config.shared_memory(true); config.wasm_threads(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ()); let module = Module::new( &engine, r#" (module (memory 1 1 shared)) "#, )?; let instance = Instance::new(&mut store, &module, &[])?; let m = instance.debug_shared_memory(&mut store, 0).unwrap(); let unsafe_cell = &m.data()[1024]; assert_eq!(unsafe { *unsafe_cell.get() }, 0); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn all_instances_and_modules_in_store() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ()); let m1 = Module::new( &engine, r#" (module (func (param i32) (result i32) (local.get 0))) "#, )?; let m2 = Module::new( &engine, r#" (module (func (param i32) (result i32) (local.get 0))) "#, )?; let i1 = Instance::new(&mut store, &m1, &[])?; let i2 = Instance::new(&mut store, &m2, &[])?; let instances = store.debug_all_instances(); let modules = store.debug_all_modules(); assert_eq!(instances.len(), 2); assert_eq!(modules.len(), 2); assert!( (Module::same(&modules[0], &m1) && Module::same(&modules[1], &m2)) || (Module::same(&modules[1], &m1) && Module::same(&modules[0], &m2)) ); assert!(instances[0] == i1); assert!(instances[1] == i2); Ok(()) } macro_rules! debug_event_checker { ($ty:tt, $store:tt, $( { $i:expr ; $pat:pat => $body:tt } ),*) => { #[derive(Clone)] struct $ty(Arc); impl $ty { fn new_and_counter() -> (Self, Arc) { let counter = Arc::new(AtomicUsize::new(0)); let counter_clone = counter.clone(); ($ty(counter), counter_clone) } } impl DebugHandler for $ty { type Data = (); fn handle( &self, #[allow(unused_variables, reason = "macro rules")] #[allow(unused_mut, reason = "macro rules")] mut $store: StoreContextMut<'_, ()>, event: DebugEvent<'_>, ) -> impl Future + Send { let step = self.0.fetch_add(1, Ordering::Relaxed); async move { if false {} $( else if step == $i { match event { $pat => { $body; } _ => panic!("Incorrect event"), } } )* else { panic!("Too many steps"); } } } } } } #[tokio::test] #[cfg_attr(miri, ignore)] async fn uncaught_exception_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); }, r#" (module (tag $t (param i32)) (func (export "main") call 1) (func (local $i i32) (local.set $i (i32.const 100)) (throw $t (i32.const 42)))) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::UncaughtExceptionThrown(e) => { assert_eq!(e.field(&mut store, 0).unwrap().unwrap_i32(), 42); let stack = store.debug_exit_frames().next().unwrap(); assert_eq!(stack.num_locals(&mut store).unwrap(), 1); assert_eq!(stack.local(&mut store, 0).unwrap().unwrap_i32(), 100); let stack = stack.parent(&mut store).unwrap().unwrap(); let stack = stack.parent(&mut store).unwrap(); assert!(stack.is_none()); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = []; let result = func.call_async(&mut store, &[], &mut results).await; assert!(result.is_err()); // Uncaught exception. assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn caught_exception_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); }, r#" (module (tag $t (param i32)) (func (export "main") (block $b (result i32) (try_table (catch $t $b) call 1) i32.const 0) drop) (func (local $i i32) (local.set $i (i32.const 100)) (throw $t (i32.const 42)))) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::CaughtExceptionThrown(e) => { assert_eq!(e.field(&mut store, 0).unwrap().unwrap_i32(), 42); let stack = store.debug_exit_frames().next().unwrap(); assert_eq!(stack.num_locals(&mut store).unwrap(), 1); assert_eq!(stack.local(&mut store, 0).unwrap().unwrap_i32(), 100); let stack = stack.parent(&mut store).unwrap().unwrap(); let stack = stack.parent(&mut store).unwrap(); assert!(stack.is_none()); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = []; func.call_async(&mut store, &[], &mut results).await?; assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn hostcall_trap_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); }, r#" (module (func (export "main") (result i32) i32.const 0 i32.const 0 i32.div_u drop i32.const 42)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::Trap(wasmtime_environ::Trap::IntegerDivisionByZero) => { let frame = store.debug_exit_frames().next().unwrap(); let (_func, pc) = frame.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(pc.raw(), 0x26); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; let result = func.call_async(&mut store, &[], &mut results).await; assert!(result.is_err()); // Uncaught trap. assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn hostcall_error_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); }, r#" (module (import "" "do_a_trap" (func)) (func (export "main") call 0)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::HostcallError(e) => { assert!(format!("{e:?}").contains("secret error message")); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); let do_a_trap = Func::wrap( &mut store, |_caller: Caller<'_, ()>| -> wasmtime::Result<()> { Err(wasmtime::format_err!("secret error message")) }, ); let instance = Instance::new_async(&mut store, &module, &[Extern::Func(do_a_trap)]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = []; let result = func.call_async(&mut store, &[], &mut results).await; assert!(result.is_err()); // Uncaught trap. assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn breakpoint_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); }, r#" (module (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); assert_eq!(stack.num_locals(&mut store).unwrap(), 2); assert_eq!(stack.local(&mut store, 0).unwrap().unwrap_i32(), 1); assert_eq!(stack.local(&mut store, 1).unwrap().unwrap_i32(), 2); let (func, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(func.as_u32(), 0); assert_eq!(pc.raw(), 0x28); let stack = stack.parent(&mut store).unwrap(); assert!(stack.is_none()); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); store .edit_breakpoints() .unwrap() .add_breakpoint(&module, ModulePC::new(0x28))?; let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); assert_eq!(results[0].unwrap_i32(), 3); let breakpoints = store.breakpoints().unwrap().collect::>(); assert_eq!(breakpoints.len(), 1); assert!(Module::same(&breakpoints[0].module, &module)); assert_eq!(breakpoints[0].pc, ModulePC::new(0x28)); store .edit_breakpoints() .unwrap() .remove_breakpoint(&module, ModulePC::new(0x28))?; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); // Should not have incremented from above. assert_eq!(results[0].unwrap_i32(), 3); // Enable single-step mode (on top of the breakpoint already enabled). assert!(!store.is_single_step()); store.edit_breakpoints().unwrap().single_step(true).unwrap(); assert!(store.is_single_step()); debug_event_checker!( D2, store, { 0 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); let (_, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(pc.raw(), 0x24); } }, { 1 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); let (_, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(pc.raw(), 0x26); } }, { 2 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); let (_, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(pc.raw(), 0x28); } }, { 3 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); let (_, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(pc.raw(), 0x29); } } ); let (handler, counter) = D2::new_and_counter(); store.set_debug_handler(handler); func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 4); // Re-enable individual breakpoint. store .edit_breakpoints() .unwrap() .add_breakpoint(&module, ModulePC::new(0x28)) .unwrap(); // Now disable single-stepping. The single breakpoint set above // should still remain. store .edit_breakpoints() .unwrap() .single_step(false) .unwrap(); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn breakpoints_in_inlined_code() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.wasm_exceptions(true); config.compiler_inlining(true); unsafe { config.cranelift_flag_set("wasmtime_inlining_intra_module", "true"); } }, r#" (module (func $f (export "f") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add) (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 call $f)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::Breakpoint => {} }, { 1 ; wasmtime::DebugEvent::Breakpoint => {} } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); store .edit_breakpoints() .unwrap() .add_breakpoint(&module, ModulePC::new(0x2d))?; // `i32.add` in `$f`. let instance = Instance::new_async(&mut store, &module, &[]).await?; let func_main = instance.get_func(&mut store, "main").unwrap(); let func_f = instance.get_func(&mut store, "f").unwrap(); let mut results = [Val::I32(0)]; // Breakpoint in `$f` should have been hit in `main` even if it // was inlined. func_main .call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); assert_eq!(results[0].unwrap_i32(), 3); // Breakpoint in `$f` should be hit when called directly, too. func_f .call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 2); assert_eq!(results[0].unwrap_i32(), 3); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn epoch_events() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let (module, mut store) = get_module_and_store( |config| { config.epoch_interruption(true); }, r#" (module (func $f (export "f") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::EpochYield => {} } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); store.set_epoch_deadline(1); store.epoch_deadline_async_yield_and_update(1); store.engine().increment_epoch(); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func_f = instance.get_func(&mut store, "f").unwrap(); let mut results = [Val::I32(0)]; func_f .call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); assert_eq!(results[0].unwrap_i32(), 3); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn invalidated_frame_handles() -> wasmtime::Result<()> { let (module, mut store) = get_module_and_store( |_config| {}, r#" (module (import "" "" (func)) (func (export "main") (local i32 i32) i32.const 1 local.set 0 i32.const 2 local.set 1 call 2 call 0) (func (local i32 i32) i32.const 3 local.set 0 i32.const 4 local.set 1 call 0)) "#, )?; let handle: Arc>> = Arc::new(Mutex::new(None)); let hostfunc = Func::wrap_async(&mut store, move |mut caller, _args: ()| { let handle = handle.clone(); Box::new(async move { let frame = handle.lock().unwrap().take(); if let Some(frame) = frame { // Ensure that the handle has been invalidated. assert!(!frame.is_valid(&mut caller)); // Ensure that attempts to fetch data from the frame // fail with a clean `Err`, not a panic or crash. let result = frame.wasm_function_index_and_pc(&mut caller); assert!(result.is_err()); // Ensure that we can get a new frame handle and use it. let frame = caller.debug_exit_frames().next().unwrap(); assert_eq!(frame.num_locals(&mut caller)?, 2); assert_eq!(frame.local(&mut caller, 0)?.unwrap_i32(), 1); assert_eq!(frame.local(&mut caller, 1)?.unwrap_i32(), 2); } else { let frame = caller.debug_exit_frames().next().unwrap(); assert_eq!(frame.num_locals(&mut caller)?, 2); assert_eq!(frame.local(&mut caller, 0)?.unwrap_i32(), 3); assert_eq!(frame.local(&mut caller, 1)?.unwrap_i32(), 4); *handle.lock().unwrap() = Some(frame); } Ok(()) }) }); let instance = Instance::new_async(&mut store, &module, &[Extern::Func(hostfunc)]).await?; let main = instance.get_func(&mut store, "main").unwrap(); main.call_async(&mut store, &[], &mut []).await?; Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn invalidated_frame_handles_in_dropped_future() -> wasmtime::Result<()> { let (module, mut store) = get_module_and_store( |_config| {}, r#" (module (import "" "" (func)) (func (export "main") call 0)) "#, )?; let handle: Arc>> = Arc::new(Mutex::new(None)); let handle_clone = handle.clone(); let hostfunc = Func::wrap_async(&mut store, move |mut caller, _args: ()| { let handle_clone = handle_clone.clone(); Box::new(async move { let frame = caller.debug_exit_frames().next().unwrap(); *handle_clone.lock().unwrap() = Some(frame); tokio::task::yield_now().await; }) }); let instance = Instance::new_async(&mut store, &module, &[Extern::Func(hostfunc)]).await?; let main = instance.get_func(&mut store, "main").unwrap(); let future = Box::pin(main.call_async(&mut store, &[], &mut [])); // Poll once, then drop. let poll_once = PollOnce::new(future); let future = poll_once.await; drop(future); // The frame handle should now be invalid. let mut handle = handle.lock().unwrap(); let frame = handle.take().unwrap(); assert!(!frame.is_valid(&mut store)); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn module_bytecode() -> wasmtime::Result<()> { let wasm = wat::parse_str( r#" (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add ) ) "#, ) .unwrap(); let mut config = Config::default(); config.guest_debug(true); let engine = Engine::new(&config)?; let module = Module::new(&engine, &wasm)?; assert_eq!(module.debug_bytecode(), Some(&wasm[..])); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn module_bytecode_absent_without_debug() -> wasmtime::Result<()> { let wasm = wat::parse_str("(module)").unwrap(); let mut config = Config::default(); config.guest_debug(false); let engine = Engine::new(&config)?; let module = Module::new(&engine, &wasm)?; assert_eq!(module.debug_bytecode(), None); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn component_bytecode() -> wasmtime::Result<()> { use wasmtime::component::{Component, Linker}; // Build the bytecode for each core module by compiling them // standalone. let m1_body = r#"(func (export "f1") (result i32) i32.const 42)"#; let m2_body = r#"(func (export "f2") (result i32) i32.const 99)"#; let m1_wasm = wat::parse_str(&format!("(module $m1 {m1_body})")).unwrap(); let m2_wasm = wat::parse_str(&format!("(module $m2 {m2_body})")).unwrap(); // Build a component that embeds both core modules inline. let component_wasm = wat::parse_str(&format!( r#"(component (core module $m1 {m1_body}) (core instance $i1 (instantiate (module $m1))) (core module $m2 {m2_body}) (core instance $i2 (instantiate (module $m2)))) "#, )) .unwrap(); let mut config = Config::default(); config.guest_debug(true); let engine = Engine::new(&config)?; let component = Component::new(&engine, &component_wasm)?; let linker: Linker<()> = Linker::new(&engine); let mut store = Store::new(&engine, ()); linker.instantiate(&mut store, &component)?; let modules = store.debug_all_modules(); assert_eq!(modules.len(), 2); // Modules should be registered in offset order. The API doesn't // guarantee this, but this suffices for a test. assert_eq!(modules[0].debug_bytecode().unwrap(), &m1_wasm[..]); assert_eq!(modules[1].debug_bytecode().unwrap(), &m2_wasm[..]); Ok(()) } #[test] #[cfg_attr(miri, ignore)] fn debug_ids() -> wasmtime::Result<()> { let mut config = Config::default(); config.guest_debug(true); config.wasm_exceptions(true); let engine = Engine::new(&config)?; let mut store = Store::new(&engine, ()); let module1 = Module::new( &engine, r#" (module (memory 1 1) (memory 1 1) (global (mut i32) (i32.const 0)) (global (mut i32) (i32.const 1)) (table 1 1 funcref) (table 1 1 funcref) (tag (param i32)) (tag (param i64))) "#, )?; let module2 = Module::new( &engine, r#" (module (memory (export "m") 1 1)) "#, )?; let instance1 = Instance::new(&mut store, &module1, &[])?; let instance2 = Instance::new(&mut store, &module2, &[])?; let instance3 = Instance::new(&mut store, &module1, &[])?; assert_ne!( module1.debug_index_in_engine(), module2.debug_index_in_engine() ); assert_ne!( instance1.debug_index_in_store(), instance2.debug_index_in_store() ); assert_ne!( instance1 .debug_memory(&mut store, 0) .unwrap() .debug_index_in_store(), instance1 .debug_memory(&mut store, 1) .unwrap() .debug_index_in_store() ); assert_ne!( instance1 .debug_memory(&mut store, 0) .unwrap() .debug_index_in_store(), instance2 .debug_memory(&mut store, 0) .unwrap() .debug_index_in_store() ); assert_ne!( instance1 .debug_memory(&mut store, 0) .unwrap() .debug_index_in_store(), instance3 .debug_memory(&mut store, 0) .unwrap() .debug_index_in_store() ); assert_ne!( instance1 .debug_global(&mut store, 0) .unwrap() .debug_index_in_store(), instance3 .debug_global(&mut store, 0) .unwrap() .debug_index_in_store() ); assert_ne!( instance1 .debug_table(&mut store, 0) .unwrap() .debug_index_in_store(), instance3 .debug_table(&mut store, 0) .unwrap() .debug_index_in_store() ); assert_ne!( instance1 .debug_tag(&mut store, 0) .unwrap() .debug_index_in_store(), instance3 .debug_tag(&mut store, 0) .unwrap() .debug_index_in_store() ); let m_via_export = instance2 .get_export(&mut store, "m") .unwrap() .into_memory() .unwrap(); let m_via_introspection = instance2.debug_memory(&mut store, 0).unwrap(); assert_eq!( m_via_export.debug_index_in_store(), m_via_introspection.debug_index_in_store() ); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn single_step_before_instantiation() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let mut config = Config::default(); 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, ()); // Enable single-stepping *before* instantiation. The module has not // been registered with this store yet. store.edit_breakpoints().unwrap().single_step(true).unwrap(); assert!(store.is_single_step()); #[derive(Clone)] struct CountingHandler(Arc); impl DebugHandler for CountingHandler { type Data = (); async fn handle(&self, _store: StoreContextMut<'_, ()>, event: DebugEvent<'_>) { match event { DebugEvent::Breakpoint => { self.0.fetch_add(1, Ordering::Relaxed); } _ => {} } } } let counter = Arc::new(AtomicUsize::new(0)); store.set_debug_handler(CountingHandler(counter.clone())); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(results[0].unwrap_i32(), 3); assert_eq!(counter.load(Ordering::Relaxed), 4); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn early_epoch_yield_still_has_vmctx() -> wasmtime::Result<()> { let _ = env_logger::try_init(); let mut config = Config::default(); config.guest_debug(true); config.epoch_interruption(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, ()); store.set_epoch_deadline(1); store.epoch_deadline_async_yield_and_update(1); engine.increment_epoch(); #[derive(Clone)] struct H; impl DebugHandler for H { type Data = (); async fn handle(&self, mut store: StoreContextMut<'_, ()>, _event: DebugEvent<'_>) { // Ensure we can access the instance (which accesses the // vmctx slot in the frame's debug info). let frame = store.debug_exit_frames().next().unwrap(); let _instance = frame.instance(&mut store); } } store.set_debug_handler(H); let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(results[0].unwrap_i32(), 3); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn breakpoint_slips_to_first_opcode() -> wasmtime::Result<()> { let _ = env_logger::try_init(); // Breakpoints set at the function body start (which includes the // local declarations and precedes the first opcode) should be // "slipped" forward to the first opcode. This matches how LLDB // sets breakpoints using DWARF `DW_AT_low_pc`. // // For the WAT below, `wasm-objdump -d` shows: // // ``` // 000023 func[0]
: // 000024: 20 00 | local.get 0 // 000026: 20 01 | local.get 1 // 000028: 6a | i32.add // 000029: 0b | end // ``` // // 0x23 is the function body start (locals count byte), while 0x24 // is the first opcode. Setting a breakpoint at 0x23 should slip // to 0x24. let (module, mut store) = get_module_and_store( |_config| {}, r#" (module (func (export "main") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) "#, )?; debug_event_checker!( D, store, { 0 ; wasmtime::DebugEvent::Breakpoint => { let stack = store.debug_exit_frames().next().unwrap(); let (func, pc) = stack.wasm_function_index_and_pc(&mut store).unwrap().unwrap(); assert_eq!(func.as_u32(), 0); // The breakpoint should fire at the first opcode // (0x24), not at the function body start (0x23). assert_eq!(pc.raw(), 0x24); } } ); let (handler, counter) = D::new_and_counter(); store.set_debug_handler(handler); store .edit_breakpoints() .unwrap() .add_breakpoint(&module, ModulePC::new(0x23))?; let instance = Instance::new_async(&mut store, &module, &[]).await?; let func = instance.get_func(&mut store, "main").unwrap(); let mut results = [Val::I32(0)]; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; assert_eq!(counter.load(Ordering::Relaxed), 1); assert_eq!(results[0].unwrap_i32(), 3); // The actual breakpoint stored should be at the slipped PC. let breakpoints = store.breakpoints().unwrap().collect::>(); assert_eq!(breakpoints.len(), 1); assert_eq!(breakpoints[0].pc, ModulePC::new(0x24)); // Removing with the originally requested PC should work. store .edit_breakpoints() .unwrap() .remove_breakpoint(&module, ModulePC::new(0x23))?; func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results) .await?; // Counter should not have incremented now that we removed the // breakpoint. assert_eq!(counter.load(Ordering::Relaxed), 1); Ok(()) } #[tokio::test] #[cfg_attr(miri, ignore)] async fn component_module_relative_breakpoint_pcs() -> wasmtime::Result<()> { use wasmtime::component::{Component, Linker}; let _ = env_logger::try_init(); let m1_body = r#"(func (export "f1") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)"#; let m2_body = r#"(func (export "f2") (param i32 i32) (result i32) local.get 0 local.get 1 i32.mul)"#; let _m1_wasm = wat::parse_str(&format!("(module {m1_body})"))?; let _m2_wasm = wat::parse_str(&format!("(module {m2_body})"))?; let component_wat = format!( r#"(component (core module $m1 {m1_body}) (core instance $i1 (instantiate $m1)) (core module $m2 {m2_body}) (core instance $i2 (instantiate $m2)) (func (export "f1") (param "a" s32) (param "b" s32) (result s32) (canon lift (core func $i1 "f1"))) (func (export "f2") (param "a" s32) (param "b" s32) (result s32) (canon lift (core func $i2 "f2"))))"#, ); let mut config = Config::default(); config.guest_debug(true); let engine = Engine::new(&config)?; let component = Component::new(&engine, &component_wat)?; let linker: Linker<()> = Linker::new(&engine); let mut store = Store::new(&engine, ()); let instance = linker.instantiate_async(&mut store, &component).await?; let modules = store.debug_all_modules(); assert_eq!(modules.len(), 2); // The i32.add / i32.mul instruction is at module-relative offset // 0x26 in both modules. let breakpoint_pc = ModulePC::new(0x26); // Record breakpoint PCs seen in each event. let observed_pcs = Arc::new(Mutex::new(Vec::<(u32, u32)>::new())); let observed_pcs_clone = observed_pcs.clone(); #[derive(Clone)] struct D(Arc>>); impl DebugHandler for D { type Data = (); fn handle( &self, mut store: StoreContextMut<'_, ()>, _event: DebugEvent<'_>, ) -> impl std::future::Future + Send { let frame = store.debug_exit_frames().next().unwrap(); let (func, pc) = frame .wasm_function_index_and_pc(&mut store) .unwrap() .unwrap(); self.0.lock().unwrap().push((func.as_u32(), pc.raw())); async {} } } store.set_debug_handler(D(observed_pcs_clone)); // Set breakpoints at the same module-relative PC (0x26) in both // modules. store .edit_breakpoints() .unwrap() .add_breakpoint(&modules[0], breakpoint_pc)?; store .edit_breakpoints() .unwrap() .add_breakpoint(&modules[1], breakpoint_pc)?; let f1 = instance.get_typed_func::<(i32, i32), (i32,)>(&mut store, "f1")?; let (result,) = f1.call_async(&mut store, (3, 5)).await?; assert_eq!(result, 8); let f2 = instance.get_typed_func::<(i32, i32), (i32,)>(&mut store, "f2")?; let (result,) = f2.call_async(&mut store, (3, 5)).await?; assert_eq!(result, 15); // Both breakpoint PCs should be 0x26 (module-relative). let pcs = observed_pcs.lock().unwrap(); assert_eq!(pcs.len(), 2); assert_eq!(pcs[0], (0, 0x26)); assert_eq!(pcs[1], (0, 0x26)); Ok(()) }