diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs index 9b2733f515a5..411dc144ffd0 100644 --- a/crates/environ/src/compile/module_environ.rs +++ b/crates/environ/src/compile/module_environ.rs @@ -608,6 +608,12 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { params: sig.params().into(), }); } + if self.tunables.debug_guest { + // All functions are potentially reachable and + // callable by the guest debugger, so they must + // all be flagged as escaping. + self.flag_func_escaped(func_index); + } self.result .function_body_inputs .push(FunctionBodyData { validator, body }); diff --git a/crates/wasmtime/src/runtime/debug.rs b/crates/wasmtime/src/runtime/debug.rs index 3138465ae0ae..951c81a1d2ab 100644 --- a/crates/wasmtime/src/runtime/debug.rs +++ b/crates/wasmtime/src/runtime/debug.rs @@ -17,7 +17,8 @@ use core::{ffi::c_void, ptr::NonNull}; use wasmtime_environ::FrameTable; use wasmtime_environ::{ DefinedFuncIndex, FrameInstPos, FrameStackShape, FrameStateSlot, FrameStateSlotOffset, - FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType, FuncKey, Trap, + FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType, FuncIndex, FuncKey, + GlobalIndex, MemoryIndex, TableIndex, TagIndex, Trap, }; use wasmtime_unwinder::Frame; @@ -65,6 +66,228 @@ impl<'a, T> StoreContextMut<'a, T> { } } +impl Instance { + /// Get access to a global within this instance's globals index + /// space. + /// + /// This permits accessing globals whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any global index that is out-of-bounds. + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_global( + &self, + mut store: impl AsContextMut, + global_index: u32, + ) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let instance = &store[self.id]; + let env_module = self.module(&store).env_module(); + // N.B.: `from_bits` here rather than `from_u32` so we don't + // panic on `u32::MAX`. A `u32::MAX` will become an invalid + // entity index which will properly return `None` below. + let global = GlobalIndex::from_bits(global_index); + if env_module.globals.is_valid(global) { + Some(instance.get_exported_global(store.id(), global)) + } else { + None + } + } + + /// Get access to a memory (unshared only) within this instance's + /// memory index space. + /// + /// This permits accessing memories whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any memory index that is out-of-bounds. + /// + /// `None` is returned for any shared memory (use + /// `debug_shared_memory` instead). + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_memory( + &self, + mut store: impl AsContextMut, + memory_index: u32, + ) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let instance = &store[self.id]; + let env_module = self.module(&store).env_module(); + let memory = MemoryIndex::from_bits(memory_index); + if env_module.memories.is_valid(memory) { + Some( + instance + .get_exported_memory(store.id(), memory) + .unshared()?, + ) + } else { + None + } + } + + /// Get access to a shared memory within this instance's memory + /// index space. + /// + /// This permits accessing memories whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any memory index that is out-of-bounds. + /// + /// `None` is returned for any unshared memory (use `debug_memory` + /// instead). + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_shared_memory( + &self, + mut store: impl AsContextMut, + memory_index: u32, + ) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let instance = &store[self.id]; + let env_module = self.module(&store).env_module(); + let memory = MemoryIndex::from_bits(memory_index); + if env_module.memories.is_valid(memory) { + Some(crate::SharedMemory::from_raw( + instance.get_exported_memory(store.id(), memory).shared()?, + store.engine().clone(), + )) + } else { + None + } + } + + /// Get access to a table within this instance's table index + /// space. + /// + /// This permits accessing tables whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any table index that is out-of-bounds. + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_table( + &self, + mut store: impl AsContextMut, + table_index: u32, + ) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let instance = &store[self.id]; + let env_module = self.module(&store).env_module(); + let table = TableIndex::from_bits(table_index); + if env_module.tables.is_valid(table) { + Some(instance.get_exported_table(store.id(), table)) + } else { + None + } + } + + /// Get access to a function within this instance's function index + /// space. + /// + /// This permits accessing functions whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any function index that is + /// out-of-bounds. + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_function( + &self, + mut store: impl AsContextMut, + function_index: u32, + ) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let env_module = self.module(&store).env_module(); + let func = FuncIndex::from_bits(function_index); + if env_module.functions.is_valid(func) { + let store_id = store.id(); + let (instance, registry) = store.instance_and_module_registry_mut(self.id()); + // SAFETY: the `store` and `registry` are associated with + // this instance as we fetched teh instance directly from + // the store above. + unsafe { Some(instance.get_exported_func(registry, store_id, func)) } + } else { + None + } + } + + /// Get access to a tag within this instance's tag index space. + /// + /// This permits accessing tags whether they are exported or + /// not. However, it is only available for purposes of debugging, + /// and so is only permitted when `guest_debug` is enabled in the + /// Engine's configuration. The intent of the Wasmtime API is to + /// enforce the Wasm type system's encapsulation even in the host + /// API, except where necessary for developer tooling. + /// + /// `None` is returned for any tag index that is out-of-bounds. + /// + /// `None` is returned if guest-debugging is not enabled in the + /// engine configuration for this Store. + pub fn debug_tag(&self, mut store: impl AsContextMut, tag_index: u32) -> Option { + let store = store.as_context_mut().0; + if !store.engine().tunables().debug_guest { + return None; + } + + let instance = &store[self.id]; + let env_module = self.module(&store).env_module(); + let tag = TagIndex::from_bits(tag_index); + if env_module.tags.is_valid(tag) { + Some(instance.get_exported_tag(store.id(), tag)) + } else { + None + } + } +} + impl<'a, T> StoreContext<'a, T> { /// Return all breakpoints. pub fn breakpoints(self) -> Option + 'a> { diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index e4e496e0e92f..0e45b3ef03cf 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -36,7 +36,7 @@ use wasmtime_environ::{ #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(C)] pub struct Instance { - id: StoreInstanceId, + pub(crate) id: StoreInstanceId, } // Double-check that the C representation in `instance.h` matches our in-Rust diff --git a/crates/wasmtime/src/runtime/vm/export.rs b/crates/wasmtime/src/runtime/vm/export.rs index 4c11d9729ebd..932d5f95d9d8 100644 --- a/crates/wasmtime/src/runtime/vm/export.rs +++ b/crates/wasmtime/src/runtime/vm/export.rs @@ -33,4 +33,10 @@ impl ExportMemory { ExportMemory::Shared(..) => None, } } + pub fn shared(self) -> Option { + match self { + ExportMemory::Unshared(_) => None, + ExportMemory::Shared(m, _) => Some(m), + } + } } diff --git a/tests/all/debug.rs b/tests/all/debug.rs index 318ba1c272f2..8dbeab7f218a 100644 --- a/tests/all/debug.rs +++ b/tests/all/debug.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use wasmtime::{ AsContextMut, Caller, Config, DebugEvent, DebugHandler, Engine, Extern, FrameParentResult, - Func, Instance, Module, Store, StoreContextMut, Val, + Func, Global, GlobalType, Instance, Module, Mutability, Store, StoreContextMut, Val, ValType, }; #[test] @@ -15,6 +15,21 @@ fn debugging_does_not_work_with_signal_based_traps() { 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_frames().is_none()); + assert!(instance.debug_global(&mut store, 0).is_none()); + + Ok(()) +} + fn get_module_and_store( c: C, wat: &str, @@ -305,6 +320,133 @@ fn debug_frames_on_store_with_no_wasm_activation() -> wasmtime::Result<()> { 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(()) +} + macro_rules! debug_event_checker { ($ty:tt, $store:tt,