Skip to content

Commit b90b896

Browse files
authored
Debugging: apply single-stepping patches to modules instantiated after setting changes. (#12663)
We currently do a store-wide state-change on all registered modules when the "single stepping" flag changes, patching in or out all breakpoints. However, this design didn't account for modules registered with the store *after* single-stepping is enabled (and new modules may be registered any time an instantiation occurs). In particular this is problematic when a debugger, e.g., sets the single-step flag right at the beginning of execution of a "host main function" that calls Wasm, before the main instantiation occurs. This PR threads through the breakpoint state in the registration path, with the narrow waist at `ModuleRegistry::register` which is the only place where modules are added to the `LoadedCode`.
1 parent 4a16884 commit b90b896

6 files changed

Lines changed: 171 additions & 32 deletions

File tree

crates/wasmtime/src/runtime/component/instance.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,8 @@ impl<'a> Instantiator<'a> {
724724
imports: &'a Arc<PrimaryMap<RuntimeImportIndex, RuntimeImport>>,
725725
) -> Result<Instantiator<'a>> {
726726
let env_component = component.env_component();
727-
let (modules, engine) = store.modules_and_engine_mut();
728-
modules.register_component(component, engine)?;
727+
let (modules, engine, breakpoints) = store.modules_and_engine_and_breakpoints_mut();
728+
modules.register_component(component, engine, breakpoints)?;
729729
let imported_resources: ImportedResources =
730730
PrimaryMap::with_capacity(env_component.imported_resources.len());
731731

crates/wasmtime/src/runtime/debug.rs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Debugging API.
22
33
use super::store::AsStoreOpaque;
4+
use crate::code::StoreCode;
5+
use crate::module::RegisterBreakpointState;
46
use crate::store::StoreId;
57
use crate::vm::{Activation, Backtrace};
68
use crate::{
@@ -1007,15 +1009,34 @@ impl BreakpointState {
10071009
pub(crate) fn is_single_step(&self) -> bool {
10081010
self.single_step
10091011
}
1012+
1013+
/// Internal helper to patch a new module for
1014+
/// single-stepping. When a module is newly registered in a
1015+
/// `Store`, we need to patch all breakpoints into the copy for
1016+
/// this `Store` if single-stepping is currently enabled.
1017+
pub(crate) fn patch_new_module(&self, code: &mut StoreCode, module: &Module) -> Result<()> {
1018+
// Apply single-step state if single-stepping is enabled. Note
1019+
// that no other individual breakpoints will exist yet (as
1020+
// this is a newly registered module).
1021+
if self.single_step {
1022+
let mem = code.code_memory_mut().unwrap();
1023+
mem.unpublish()?;
1024+
BreakpointEdit::apply_single_step(mem, module, true, |_key| false)?;
1025+
mem.publish()?;
1026+
}
1027+
Ok(())
1028+
}
10101029
}
10111030

10121031
impl<'a> BreakpointEdit<'a> {
10131032
fn get_code_memory<'b>(
1033+
breakpoints: &BreakpointState,
10141034
registry: &'b mut ModuleRegistry,
10151035
dirty_modules: &mut BTreeSet<StoreCodePC>,
10161036
module: &Module,
10171037
) -> Result<&'b mut CodeMemory> {
1018-
let store_code_pc = registry.store_code_base_or_register(module)?;
1038+
let store_code_pc =
1039+
registry.store_code_base_or_register(module, RegisterBreakpointState(breakpoints))?;
10191040
let code_memory = registry
10201041
.store_code_mut(store_code_pc)
10211042
.expect("Just checked presence above")
@@ -1052,7 +1073,8 @@ impl<'a> BreakpointEdit<'a> {
10521073
let key = BreakpointKey::from_raw(module, pc);
10531074
self.state.breakpoints.insert(key);
10541075
log::trace!("patching in breakpoint {key:?}");
1055-
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, module)?;
1076+
let mem =
1077+
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?;
10561078
let frame_table = module
10571079
.frame_table()
10581080
.expect("Frame table must be present when guest-debug is enabled");
@@ -1069,7 +1091,8 @@ impl<'a> BreakpointEdit<'a> {
10691091
let key = BreakpointKey::from_raw(module, pc);
10701092
self.state.breakpoints.remove(&key);
10711093
if !self.state.single_step {
1072-
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, module)?;
1094+
let mem =
1095+
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, module)?;
10731096
let frame_table = module
10741097
.frame_table()
10751098
.expect("Frame table must be present when guest-debug is enabled");
@@ -1079,6 +1102,26 @@ impl<'a> BreakpointEdit<'a> {
10791102
Ok(())
10801103
}
10811104

1105+
fn apply_single_step<F: Fn(&BreakpointKey) -> bool>(
1106+
mem: &mut CodeMemory,
1107+
module: &Module,
1108+
enabled: bool,
1109+
key_enabled: F,
1110+
) -> Result<()> {
1111+
let table = module
1112+
.frame_table()
1113+
.expect("Frame table must be present when guest-debug is enabled");
1114+
for (wasm_pc, patch) in table.breakpoint_patches() {
1115+
let key = BreakpointKey::from_raw(&module, wasm_pc);
1116+
let this_enabled = enabled || key_enabled(&key);
1117+
log::trace!(
1118+
"single_step: enabled {enabled} key {key:?} -> this_enabled {this_enabled}"
1119+
);
1120+
Self::patch(core::iter::once(patch), mem, this_enabled);
1121+
}
1122+
Ok(())
1123+
}
1124+
10821125
/// Turn on or off single-step mode.
10831126
///
10841127
/// In single-step mode, a breakpoint event is emitted at every
@@ -1095,18 +1138,11 @@ impl<'a> BreakpointEdit<'a> {
10951138
}
10961139
let modules = self.registry.all_modules().cloned().collect::<Vec<_>>();
10971140
for module in modules {
1098-
let mem = Self::get_code_memory(self.registry, &mut self.dirty_modules, &module)?;
1099-
let table = module
1100-
.frame_table()
1101-
.expect("Frame table must be present when guest-debug is enabled");
1102-
for (wasm_pc, patch) in table.breakpoint_patches() {
1103-
let key = BreakpointKey::from_raw(&module, wasm_pc);
1104-
let this_enabled = enabled || self.state.breakpoints.contains(&key);
1105-
log::trace!(
1106-
"single_step: enabled {enabled} key {key:?} -> this_enabled {this_enabled}"
1107-
);
1108-
Self::patch(core::iter::once(patch), mem, this_enabled);
1109-
}
1141+
let mem =
1142+
Self::get_code_memory(self.state, self.registry, &mut self.dirty_modules, &module)?;
1143+
Self::apply_single_step(mem, &module, enabled, |key| {
1144+
self.state.breakpoints.contains(key)
1145+
})?;
11101146
}
11111147

11121148
self.state.single_step = enabled;

crates/wasmtime/src/runtime/instance.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ impl Instance {
225225
// Note that under normal operation this shouldn't do much as the list
226226
// of funcs-with-holes should generally be empty. As a result the
227227
// process of filling this out is not super optimized at this point.
228-
let (modules, engine) = store.modules_and_engine_mut();
229-
modules.register_module(module, engine)?;
228+
let (modules, engine, breakpoints) = store.modules_and_engine_and_breakpoints_mut();
229+
modules.register_module(module, engine, breakpoints)?;
230230
let (funcrefs, modules) = store.func_refs_and_modules();
231231
funcrefs.fill(modules);
232232

@@ -305,8 +305,8 @@ impl Instance {
305305

306306
// Register the module just before instantiation to ensure we keep the module
307307
// properly referenced while in use by the store.
308-
let (modules, engine) = store.modules_and_engine_mut();
309-
let module_id = modules.register_module(module, engine)?;
308+
let (modules, engine, breakpoints) = store.modules_and_engine_and_breakpoints_mut();
309+
let module_id = modules.register_module(module, engine, breakpoints)?;
310310

311311
// The first thing we do is issue an instance allocation request
312312
// to the instance allocator. This, on success, will give us an
@@ -966,8 +966,8 @@ fn pre_instantiate_raw(
966966
) -> Result<OwnedImports> {
967967
// Register this module and use it to fill out any funcref wasm_call holes
968968
// we can. For more comments on this see `typecheck_externs`.
969-
let (modules, engine) = store.modules_and_engine_mut();
970-
modules.register_module(module, engine)?;
969+
let (modules, engine, breakpoints) = store.modules_and_engine_and_breakpoints_mut();
970+
modules.register_module(module, engine, breakpoints)?;
971971
let (funcrefs, modules) = store.func_refs_and_modules();
972972
funcrefs.fill(modules);
973973

crates/wasmtime/src/runtime/module/registry.rs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::{Engine, prelude::*};
1010
use crate::{FrameInfo, Module, code_memory::CodeMemory};
1111
use alloc::collections::btree_map::{BTreeMap, Entry};
1212
use alloc::sync::Arc;
13+
#[cfg(not(feature = "debug"))]
14+
use core::marker::PhantomData;
1315
use core::ops::Range;
1416
use core::ptr::NonNull;
1517
use wasmtime_environ::VMSharedTypeIndex;
@@ -100,6 +102,22 @@ fn assert_no_overlap(loaded_code: &BTreeMap<StoreCodePC, LoadedCode>, range: Ran
100102
}
101103
}
102104

105+
#[cfg(feature = "debug")]
106+
pub struct RegisterBreakpointState<'a>(pub(crate) &'a crate::runtime::debug::BreakpointState);
107+
#[cfg(not(feature = "debug"))]
108+
pub struct RegisterBreakpointState<'a>(pub(crate) PhantomData<&'a ()>);
109+
110+
impl<'a> RegisterBreakpointState<'a> {
111+
#[cfg(feature = "debug")]
112+
fn update(&self, code: &mut StoreCode, module: &Module) -> Result<()> {
113+
self.0.patch_new_module(code, module)
114+
}
115+
#[cfg(not(feature = "debug"))]
116+
fn update(&self, _code: &mut StoreCode, _module: &Module) -> Result<()> {
117+
Ok(())
118+
}
119+
}
120+
103121
impl ModuleRegistry {
104122
/// Get a previously-registered module by id.
105123
pub fn module_by_id(&self, id: RegisteredModuleId) -> Option<&Module> {
@@ -140,11 +158,15 @@ impl ModuleRegistry {
140158

141159
/// Fetches the base `StoreCodePC` for a given `EngineCode` with
142160
/// `Module`, registering the module if not already registered.
143-
pub fn store_code_base_or_register(&mut self, module: &Module) -> Result<StoreCodePC> {
161+
pub fn store_code_base_or_register(
162+
&mut self,
163+
module: &Module,
164+
breakpoint_state: RegisterBreakpointState,
165+
) -> Result<StoreCodePC> {
144166
let key = module.engine_code().text_range().start;
145167
if !self.store_code.contains_key(&key) {
146168
let engine = module.engine().clone();
147-
self.register_module(module, &engine)?;
169+
self.register_module(module, &engine, breakpoint_state)?;
148170
}
149171
Ok(*self.store_code.get(&key).unwrap())
150172
}
@@ -167,14 +189,32 @@ impl ModuleRegistry {
167189
&mut self,
168190
module: &Module,
169191
engine: &Engine,
192+
breakpoint_state: RegisterBreakpointState,
170193
) -> Result<RegisteredModuleId> {
171-
self.register(module.id(), module.engine_code(), Some(module), engine)
172-
.map(|id| id.unwrap())
194+
self.register(
195+
module.id(),
196+
module.engine_code(),
197+
Some(module),
198+
engine,
199+
breakpoint_state,
200+
)
201+
.map(|id| id.unwrap())
173202
}
174203

175204
#[cfg(feature = "component-model")]
176-
pub fn register_component(&mut self, component: &Component, engine: &Engine) -> Result<()> {
177-
self.register(component.id(), component.engine_code(), None, engine)?;
205+
pub fn register_component(
206+
&mut self,
207+
component: &Component,
208+
engine: &Engine,
209+
breakpoint_state: RegisterBreakpointState,
210+
) -> Result<()> {
211+
self.register(
212+
component.id(),
213+
component.engine_code(),
214+
None,
215+
engine,
216+
breakpoint_state,
217+
)?;
178218
Ok(())
179219
}
180220

@@ -185,6 +225,7 @@ impl ModuleRegistry {
185225
code: &Arc<EngineCode>,
186226
module: Option<&Module>,
187227
engine: &Engine,
228+
breakpoint_state: RegisterBreakpointState,
188229
) -> Result<Option<RegisteredModuleId>> {
189230
// Register the module, if any.
190231
let id = module.map(|module| {
@@ -219,6 +260,7 @@ impl ModuleRegistry {
219260
.get_mut(&store_code_pc)
220261
.expect("loaded_code must have entry for StoreCodePC");
221262
loaded_code.modules.insert(range.start, id);
263+
breakpoint_state.update(&mut loaded_code.code, module)?;
222264
}
223265
}
224266

crates/wasmtime/src/runtime/store.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ use crate::ThrownException;
8484
use crate::error::OutOfMemory;
8585
#[cfg(feature = "async")]
8686
use crate::fiber;
87-
use crate::module::RegisteredModuleId;
87+
use crate::module::{RegisterBreakpointState, RegisteredModuleId};
8888
use crate::prelude::*;
8989
#[cfg(feature = "gc")]
9090
use crate::runtime::vm::GcRootsList;
@@ -1670,8 +1670,15 @@ impl StoreOpaque {
16701670
}
16711671

16721672
#[inline]
1673-
pub(crate) fn modules_and_engine_mut(&mut self) -> (&mut ModuleRegistry, &Engine) {
1674-
(&mut self.modules, &self.engine)
1673+
pub(crate) fn modules_and_engine_and_breakpoints_mut(
1674+
&mut self,
1675+
) -> (&mut ModuleRegistry, &Engine, RegisterBreakpointState<'_>) {
1676+
#[cfg(feature = "debug")]
1677+
let breakpoints = RegisterBreakpointState(&self.breakpoints);
1678+
#[cfg(not(feature = "debug"))]
1679+
let breakpoints = RegisterBreakpointState(core::marker::PhantomData);
1680+
1681+
(&mut self.modules, &self.engine, breakpoints)
16751682
}
16761683

16771684
pub(crate) fn func_refs_and_modules(&mut self) -> (&mut FuncRefs, &ModuleRegistry) {

tests/all/debug.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,3 +1318,57 @@ fn debug_ids() -> wasmtime::Result<()> {
13181318

13191319
Ok(())
13201320
}
1321+
1322+
#[tokio::test]
1323+
#[cfg_attr(miri, ignore)]
1324+
async fn single_step_before_instantiation() -> wasmtime::Result<()> {
1325+
let _ = env_logger::try_init();
1326+
1327+
let mut config = Config::default();
1328+
config.guest_debug(true);
1329+
let engine = Engine::new(&config)?;
1330+
let module = Module::new(
1331+
&engine,
1332+
r#"
1333+
(module
1334+
(func (export "main") (param i32 i32) (result i32)
1335+
local.get 0
1336+
local.get 1
1337+
i32.add))
1338+
"#,
1339+
)?;
1340+
let mut store = Store::new(&engine, ());
1341+
1342+
// Enable single-stepping *before* instantiation. The module has not
1343+
// been registered with this store yet.
1344+
store.edit_breakpoints().unwrap().single_step(true).unwrap();
1345+
assert!(store.is_single_step());
1346+
1347+
#[derive(Clone)]
1348+
struct CountingHandler(Arc<AtomicUsize>);
1349+
impl DebugHandler for CountingHandler {
1350+
type Data = ();
1351+
async fn handle(&self, _store: StoreContextMut<'_, ()>, event: DebugEvent<'_>) {
1352+
match event {
1353+
DebugEvent::Breakpoint => {
1354+
self.0.fetch_add(1, Ordering::Relaxed);
1355+
}
1356+
_ => {}
1357+
}
1358+
}
1359+
}
1360+
1361+
let counter = Arc::new(AtomicUsize::new(0));
1362+
store.set_debug_handler(CountingHandler(counter.clone()));
1363+
1364+
let instance = Instance::new_async(&mut store, &module, &[]).await?;
1365+
let func = instance.get_func(&mut store, "main").unwrap();
1366+
let mut results = [Val::I32(0)];
1367+
func.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results)
1368+
.await?;
1369+
assert_eq!(results[0].unwrap_i32(), 3);
1370+
1371+
assert_eq!(counter.load(Ordering::Relaxed), 4);
1372+
1373+
Ok(())
1374+
}

0 commit comments

Comments
 (0)