Skip to content

Commit 2326d65

Browse files
authored
Debugging: provide access to private (non-exported) entities. (#12367)
* Debugging: provide access to private (non-exported) entities. A debugger will need to access all entities (globals, tables, memories), even those that are not exported, in order to provide a full debugging experience: for example, a developer who has a debugger attached to a Wasm component will expect to be able to see data in its memory. Historically we have been very careful in Wasmtime to provide access to Wasm instances' entities only as the Wasm type system allows -- that is, only if they are exported. However, debugging is privileged -- in the same way that a native host debugger has `ptrace` and can view everything about the debuggee, we need to provide APIs for seeing through the encapsulation boundary. To ensure that this "violation of encapsulation" is scoped only to the extent needed for the legitimate need (debugging), this API is dynamically available only when `guest_debug` is configured true for a given engine. Otherwise, the accessor returns `None`. I opted not to provide a full introspection API that enumerates all of the entities as the debugger should already have access to the debuggee module and be able to enumerate the entities. Thus, the API only provides a host-API handle when asking for an entity by index in a given instance's index space. * Review feedback. * Fix tests on 32-bit build (where threads and thus shared memory are not supported)
1 parent 1af6251 commit 2326d65

5 files changed

Lines changed: 380 additions & 3 deletions

File tree

crates/environ/src/compile/module_environ.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,12 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> {
608608
params: sig.params().into(),
609609
});
610610
}
611+
if self.tunables.debug_guest {
612+
// All functions are potentially reachable and
613+
// callable by the guest debugger, so they must
614+
// all be flagged as escaping.
615+
self.flag_func_escaped(func_index);
616+
}
611617
self.result
612618
.function_body_inputs
613619
.push(FunctionBodyData { validator, body });

crates/wasmtime/src/runtime/debug.rs

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use core::{ffi::c_void, ptr::NonNull};
1717
use wasmtime_environ::FrameTable;
1818
use wasmtime_environ::{
1919
DefinedFuncIndex, FrameInstPos, FrameStackShape, FrameStateSlot, FrameStateSlotOffset,
20-
FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType, FuncKey, Trap,
20+
FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType, FuncIndex, FuncKey,
21+
GlobalIndex, MemoryIndex, TableIndex, TagIndex, Trap,
2122
};
2223
use wasmtime_unwinder::Frame;
2324

@@ -65,6 +66,228 @@ impl<'a, T> StoreContextMut<'a, T> {
6566
}
6667
}
6768

69+
impl Instance {
70+
/// Get access to a global within this instance's globals index
71+
/// space.
72+
///
73+
/// This permits accessing globals whether they are exported or
74+
/// not. However, it is only available for purposes of debugging,
75+
/// and so is only permitted when `guest_debug` is enabled in the
76+
/// Engine's configuration. The intent of the Wasmtime API is to
77+
/// enforce the Wasm type system's encapsulation even in the host
78+
/// API, except where necessary for developer tooling.
79+
///
80+
/// `None` is returned for any global index that is out-of-bounds.
81+
///
82+
/// `None` is returned if guest-debugging is not enabled in the
83+
/// engine configuration for this Store.
84+
pub fn debug_global(
85+
&self,
86+
mut store: impl AsContextMut,
87+
global_index: u32,
88+
) -> Option<crate::Global> {
89+
let store = store.as_context_mut().0;
90+
if !store.engine().tunables().debug_guest {
91+
return None;
92+
}
93+
94+
let instance = &store[self.id];
95+
let env_module = self.module(&store).env_module();
96+
// N.B.: `from_bits` here rather than `from_u32` so we don't
97+
// panic on `u32::MAX`. A `u32::MAX` will become an invalid
98+
// entity index which will properly return `None` below.
99+
let global = GlobalIndex::from_bits(global_index);
100+
if env_module.globals.is_valid(global) {
101+
Some(instance.get_exported_global(store.id(), global))
102+
} else {
103+
None
104+
}
105+
}
106+
107+
/// Get access to a memory (unshared only) within this instance's
108+
/// memory index space.
109+
///
110+
/// This permits accessing memories whether they are exported or
111+
/// not. However, it is only available for purposes of debugging,
112+
/// and so is only permitted when `guest_debug` is enabled in the
113+
/// Engine's configuration. The intent of the Wasmtime API is to
114+
/// enforce the Wasm type system's encapsulation even in the host
115+
/// API, except where necessary for developer tooling.
116+
///
117+
/// `None` is returned for any memory index that is out-of-bounds.
118+
///
119+
/// `None` is returned for any shared memory (use
120+
/// `debug_shared_memory` instead).
121+
///
122+
/// `None` is returned if guest-debugging is not enabled in the
123+
/// engine configuration for this Store.
124+
pub fn debug_memory(
125+
&self,
126+
mut store: impl AsContextMut,
127+
memory_index: u32,
128+
) -> Option<crate::Memory> {
129+
let store = store.as_context_mut().0;
130+
if !store.engine().tunables().debug_guest {
131+
return None;
132+
}
133+
134+
let instance = &store[self.id];
135+
let env_module = self.module(&store).env_module();
136+
let memory = MemoryIndex::from_bits(memory_index);
137+
if env_module.memories.is_valid(memory) {
138+
Some(
139+
instance
140+
.get_exported_memory(store.id(), memory)
141+
.unshared()?,
142+
)
143+
} else {
144+
None
145+
}
146+
}
147+
148+
/// Get access to a shared memory within this instance's memory
149+
/// index space.
150+
///
151+
/// This permits accessing memories whether they are exported or
152+
/// not. However, it is only available for purposes of debugging,
153+
/// and so is only permitted when `guest_debug` is enabled in the
154+
/// Engine's configuration. The intent of the Wasmtime API is to
155+
/// enforce the Wasm type system's encapsulation even in the host
156+
/// API, except where necessary for developer tooling.
157+
///
158+
/// `None` is returned for any memory index that is out-of-bounds.
159+
///
160+
/// `None` is returned for any unshared memory (use `debug_memory`
161+
/// instead).
162+
///
163+
/// `None` is returned if guest-debugging is not enabled in the
164+
/// engine configuration for this Store.
165+
pub fn debug_shared_memory(
166+
&self,
167+
mut store: impl AsContextMut,
168+
memory_index: u32,
169+
) -> Option<crate::SharedMemory> {
170+
let store = store.as_context_mut().0;
171+
if !store.engine().tunables().debug_guest {
172+
return None;
173+
}
174+
175+
let instance = &store[self.id];
176+
let env_module = self.module(&store).env_module();
177+
let memory = MemoryIndex::from_bits(memory_index);
178+
if env_module.memories.is_valid(memory) {
179+
Some(crate::SharedMemory::from_raw(
180+
instance.get_exported_memory(store.id(), memory).shared()?,
181+
store.engine().clone(),
182+
))
183+
} else {
184+
None
185+
}
186+
}
187+
188+
/// Get access to a table within this instance's table index
189+
/// space.
190+
///
191+
/// This permits accessing tables whether they are exported or
192+
/// not. However, it is only available for purposes of debugging,
193+
/// and so is only permitted when `guest_debug` is enabled in the
194+
/// Engine's configuration. The intent of the Wasmtime API is to
195+
/// enforce the Wasm type system's encapsulation even in the host
196+
/// API, except where necessary for developer tooling.
197+
///
198+
/// `None` is returned for any table index that is out-of-bounds.
199+
///
200+
/// `None` is returned if guest-debugging is not enabled in the
201+
/// engine configuration for this Store.
202+
pub fn debug_table(
203+
&self,
204+
mut store: impl AsContextMut,
205+
table_index: u32,
206+
) -> Option<crate::Table> {
207+
let store = store.as_context_mut().0;
208+
if !store.engine().tunables().debug_guest {
209+
return None;
210+
}
211+
212+
let instance = &store[self.id];
213+
let env_module = self.module(&store).env_module();
214+
let table = TableIndex::from_bits(table_index);
215+
if env_module.tables.is_valid(table) {
216+
Some(instance.get_exported_table(store.id(), table))
217+
} else {
218+
None
219+
}
220+
}
221+
222+
/// Get access to a function within this instance's function index
223+
/// space.
224+
///
225+
/// This permits accessing functions whether they are exported or
226+
/// not. However, it is only available for purposes of debugging,
227+
/// and so is only permitted when `guest_debug` is enabled in the
228+
/// Engine's configuration. The intent of the Wasmtime API is to
229+
/// enforce the Wasm type system's encapsulation even in the host
230+
/// API, except where necessary for developer tooling.
231+
///
232+
/// `None` is returned for any function index that is
233+
/// out-of-bounds.
234+
///
235+
/// `None` is returned if guest-debugging is not enabled in the
236+
/// engine configuration for this Store.
237+
pub fn debug_function(
238+
&self,
239+
mut store: impl AsContextMut,
240+
function_index: u32,
241+
) -> Option<crate::Func> {
242+
let store = store.as_context_mut().0;
243+
if !store.engine().tunables().debug_guest {
244+
return None;
245+
}
246+
247+
let env_module = self.module(&store).env_module();
248+
let func = FuncIndex::from_bits(function_index);
249+
if env_module.functions.is_valid(func) {
250+
let store_id = store.id();
251+
let (instance, registry) = store.instance_and_module_registry_mut(self.id());
252+
// SAFETY: the `store` and `registry` are associated with
253+
// this instance as we fetched teh instance directly from
254+
// the store above.
255+
unsafe { Some(instance.get_exported_func(registry, store_id, func)) }
256+
} else {
257+
None
258+
}
259+
}
260+
261+
/// Get access to a tag within this instance's tag index space.
262+
///
263+
/// This permits accessing tags whether they are exported or
264+
/// not. However, it is only available for purposes of debugging,
265+
/// and so is only permitted when `guest_debug` is enabled in the
266+
/// Engine's configuration. The intent of the Wasmtime API is to
267+
/// enforce the Wasm type system's encapsulation even in the host
268+
/// API, except where necessary for developer tooling.
269+
///
270+
/// `None` is returned for any tag index that is out-of-bounds.
271+
///
272+
/// `None` is returned if guest-debugging is not enabled in the
273+
/// engine configuration for this Store.
274+
pub fn debug_tag(&self, mut store: impl AsContextMut, tag_index: u32) -> Option<crate::Tag> {
275+
let store = store.as_context_mut().0;
276+
if !store.engine().tunables().debug_guest {
277+
return None;
278+
}
279+
280+
let instance = &store[self.id];
281+
let env_module = self.module(&store).env_module();
282+
let tag = TagIndex::from_bits(tag_index);
283+
if env_module.tags.is_valid(tag) {
284+
Some(instance.get_exported_tag(store.id(), tag))
285+
} else {
286+
None
287+
}
288+
}
289+
}
290+
68291
impl<'a, T> StoreContext<'a, T> {
69292
/// Return all breakpoints.
70293
pub fn breakpoints(self) -> Option<impl Iterator<Item = Breakpoint> + 'a> {

crates/wasmtime/src/runtime/instance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use wasmtime_environ::{
3636
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
3737
#[repr(C)]
3838
pub struct Instance {
39-
id: StoreInstanceId,
39+
pub(crate) id: StoreInstanceId,
4040
}
4141

4242
// Double-check that the C representation in `instance.h` matches our in-Rust

crates/wasmtime/src/runtime/vm/export.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ impl ExportMemory {
3333
ExportMemory::Shared(..) => None,
3434
}
3535
}
36+
pub fn shared(self) -> Option<SharedMemory> {
37+
match self {
38+
ExportMemory::Unshared(_) => None,
39+
ExportMemory::Shared(m, _) => Some(m),
40+
}
41+
}
3642
}

0 commit comments

Comments
 (0)