Skip to content

Commit 23b2fe3

Browse files
authored
Debugging: allow frame cursor to visit all activations. (#12176)
* Debugging: allow frame cursor to visit all activations. In the initial design for the `DebugFrameCursor`, I was concerned about the effects of host async on the stability of visiting earlier activations (see also the discussion of async combinators in #11896). The basic hypothesized problem was that when Wasm calls host-code calls Wasm, the sequence of activations on the stack is not even stable between async polls; so any debugger hook, which is an async function, should not be allowed to hold a frame cursor across a yield point since it could become invalidated if the next poll stacks up the activations differently. In further conversations it's become clear that this is not actually a possibility, for the simple reason that the inner re-entrant activations into the same store take full ownership (mutably reborrow) that store, and that mut reborrow becomes part of the future; so the exact chain of activations will remain in the same sequence when re-polled. Said another way, it is impossible at any given level of async host-code to create *more than one* future that re-enters the same store and somehow poll those in different orders at different times. The worst that a host-code async combinator can do is drop the future that re-enters the store. This drops and invalidates whatever frames a cursor held over a yield might be referencing, but it *also* drops the async invocation of the debugger hook itself, and due to lifetimes the cursor cannot escape that hook, so everything is still sound. This PR thus updates the `DebugFrameCursor` to visit all activations. I've generalized the backtrace code a bit to enable this, and built an internal `StoreBacktrace` that is an iterator over all activations associated with the store. At the `DebugFrameCursor` (public API) level, the two basic choices were to present a sentinel for host frame(s) explicitly and make all Wasm-specific accessors return `Option<T>`, or skip over host frames. I opted for the latter, with `move_to_parent()` returning an enum value now that indicates whether it moved to a new activation. A note regarding the *async* component ABI: once debugging is possible within a `run_concurrent` environment, it will again be the case that a single frame cursor should see only one activation, because each (re)-entry into the store becomes a new task, if my understanding is correct. At that time, we should build an API that lets the debugger see the activation for each task separately. That's a simpler model ultimately, and it will be nice when we move to it, but as long as we have the sync component ABI with async host code and the ability to stack activations as we do today, we need to provide the debugger this visibility. (Aside: why does the debugger *need* to see more than one activation? In addition to presenting a weird and incoherent view of the world to the user if we don't, it is also necessary to implement the "next" (step-over) debugger action, because otherwise a call to a host function that re-enters the store may lead to a state with fewer, but completely disjoint, stack frames on the "one latest activation" from which it's not possible to reason about whether we've left the called-into function yet.) * Review feedback. * cargo fmt.
1 parent dde1ada commit 23b2fe3

File tree

6 files changed

+414
-87
lines changed

6 files changed

+414
-87
lines changed

crates/wasmtime/src/runtime/debug.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
code::StoreCodePC,
77
module::ModuleRegistry,
88
store::{AutoAssertNoGc, StoreOpaque},
9-
vm::{CompiledModuleId, CurrentActivationBacktrace, VMContext},
9+
vm::{CompiledModuleId, FrameOrHostCode, StoreBacktrace, VMContext},
1010
};
1111
use alloc::collections::BTreeSet;
1212
use alloc::vec;
@@ -43,11 +43,7 @@ impl<'a, T> StoreContextMut<'a, T> {
4343
return None;
4444
}
4545

46-
// SAFETY: This takes a mutable borrow of `self` (the
47-
// `StoreOpaque`), which owns all active stacks in the
48-
// store. We do not provide any API that could mutate the
49-
// frames that we are walking on the `DebugFrameCursor`.
50-
let iter = unsafe { CurrentActivationBacktrace::new(self) };
46+
let iter = StoreBacktrace::new(self);
5147
let mut view = DebugFrameCursor {
5248
iter,
5349
is_trapping_frame: false,
@@ -90,14 +86,14 @@ impl<'a, T> StoreContext<'a, T> {
9086
/// A view of an active stack frame, with the ability to move up the
9187
/// stack.
9288
///
93-
/// See the documentation on `Store::stack_value` for more information
89+
/// See the documentation on `Store::debug_frames` for more information
9490
/// about which frames this view will show.
9591
pub struct DebugFrameCursor<'a, T: 'static> {
9692
/// Iterator over frames.
9793
///
9894
/// This iterator owns the store while the view exists (accessible
9995
/// as `iter.store`).
100-
iter: CurrentActivationBacktrace<'a, T>,
96+
iter: StoreBacktrace<'a, T>,
10197

10298
/// Is the next frame to be visited by the iterator a trapping
10399
/// frame?
@@ -120,9 +116,25 @@ pub struct DebugFrameCursor<'a, T: 'static> {
120116
current: Option<FrameData>,
121117
}
122118

119+
/// The result type from `DebugFrameCursor::move_to_parent()`:
120+
/// indicates whether the cursor skipped over host code to move to the
121+
/// next Wasm frame.
122+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123+
pub enum FrameParentResult {
124+
/// The new frame is in the same Wasm activation.
125+
SameActivation,
126+
/// The new frame is in the next higher Wasm activation on the
127+
/// stack.
128+
NewActivation,
129+
}
130+
123131
impl<'a, T: 'static> DebugFrameCursor<'a, T> {
124132
/// Move up to the next frame in the activation.
125-
pub fn move_to_parent(&mut self) {
133+
///
134+
/// Returns `FrameParentMove` as an indication whether the
135+
/// moved-to frame is in the same activation or skipped over host
136+
/// code.
137+
pub fn move_to_parent(&mut self) -> FrameParentResult {
126138
// If there are no virtual frames to yield, take and decode
127139
// the next physical frame.
128140
//
@@ -133,22 +145,35 @@ impl<'a, T: 'static> DebugFrameCursor<'a, T> {
133145
// functions or other things that we completely ignore). If
134146
// this ever changes, we can remove the assert and convert
135147
// this to a loop that polls until it finds virtual frames.
148+
let mut result = FrameParentResult::SameActivation;
136149
self.current = None;
137-
if self.frames.is_empty() {
150+
while self.frames.is_empty() {
138151
let Some(next_frame) = self.iter.next() else {
139-
return;
152+
return result;
153+
};
154+
self.frames = match next_frame {
155+
FrameOrHostCode::Frame(frame) => VirtualFrame::decode(
156+
// SAFETY: we are using the Store only to decode
157+
// frames; the only mutable aspect used here is
158+
// rooting any new GC values that are read out,
159+
// and we do not remove frames that we may be
160+
// visiting.
161+
unsafe { self.iter.store_mut() }.0.as_store_opaque(),
162+
frame,
163+
self.is_trapping_frame,
164+
),
165+
FrameOrHostCode::HostCode => {
166+
result = FrameParentResult::NewActivation;
167+
continue;
168+
}
140169
};
141-
self.frames = VirtualFrame::decode(
142-
self.iter.store.0.as_store_opaque(),
143-
next_frame,
144-
self.is_trapping_frame,
145-
);
146170
debug_assert!(!self.frames.is_empty());
147171
self.is_trapping_frame = false;
148172
}
149173

150174
// Take a frame and focus it as the current one.
151175
self.current = self.frames.pop().map(|vf| FrameData::compute(vf));
176+
result
152177
}
153178

154179
/// Has the iterator reached the end of the activation?
@@ -178,7 +203,14 @@ impl<'a, T: 'static> DebugFrameCursor<'a, T> {
178203
/// Get the instance associated with the current frame.
179204
pub fn instance(&mut self) -> Instance {
180205
let instance = self.raw_instance();
181-
Instance::from_wasmtime(instance.id(), self.iter.store.0.as_store_opaque())
206+
Instance::from_wasmtime(
207+
instance.id(),
208+
// SAFETY: we are using the Store only
209+
// to read the instance reference; we
210+
// do not remove frames that we may be
211+
// visiting.
212+
unsafe { self.iter.store_mut() }.0.as_store_opaque(),
213+
)
182214
}
183215

184216
/// Get the module associated with the current frame, if any
@@ -229,7 +261,7 @@ impl<'a, T: 'static> DebugFrameCursor<'a, T> {
229261
let slot_addr = data.slot_addr;
230262
// SAFETY: compiler produced metadata to describe this local
231263
// slot and stored a value of the correct type into it.
232-
unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
264+
unsafe { read_value(&mut self.iter.store_mut().0, slot_addr, offset, ty) }
233265
}
234266

235267
/// Get the type and value of the given operand-stack value in
@@ -246,7 +278,7 @@ impl<'a, T: 'static> DebugFrameCursor<'a, T> {
246278
// SAFETY: compiler produced metadata to describe this
247279
// operand-stack slot and stored a value of the correct type
248280
// into it.
249-
unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
281+
unsafe { read_value(&mut self.iter.store_mut().0, slot_addr, offset, ty) }
250282
}
251283
}
252284

@@ -468,12 +500,15 @@ pub(crate) fn gc_refs_in_frame<'a>(ft: FrameTable<'a>, pc: u32, fp: *mut usize)
468500
impl<'a, T: 'static> AsContext for DebugFrameCursor<'a, T> {
469501
type Data = T;
470502
fn as_context(&self) -> StoreContext<'_, Self::Data> {
471-
StoreContext(self.iter.store.0)
503+
StoreContext(self.iter.store().0)
472504
}
473505
}
474506
impl<'a, T: 'static> AsContextMut for DebugFrameCursor<'a, T> {
475507
fn as_context_mut(&mut self) -> StoreContextMut<'_, Self::Data> {
476-
StoreContextMut(self.iter.store.0)
508+
// SAFETY: `StoreContextMut` does not provide any methods that
509+
// could remove frames from the stack, so the iterator remains
510+
// valid.
511+
unsafe { StoreContextMut(self.iter.store_mut().0) }
477512
}
478513
}
479514

crates/wasmtime/src/runtime/instance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use wasmtime_environ::{
3333
/// [`Linker::instantiate`](crate::Linker::instantiate) or similar
3434
/// [`Linker`](crate::Linker) methods, but a more low-level constructor is also
3535
/// available as [`Instance::new`].
36-
#[derive(Copy, Clone, Debug)]
36+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
3737
#[repr(C)]
3838
pub struct Instance {
3939
id: StoreInstanceId,

crates/wasmtime/src/runtime/store.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,11 +1256,6 @@ impl<T> Store<T> {
12561256
/// VM-level values (locals and operand stack), when debugging is
12571257
/// enabled.
12581258
///
1259-
/// This object views the frames from the most recent Wasm entry
1260-
/// onward (up to the exit that allows this host code to run). Any
1261-
/// Wasm stack frames upward from the most recent entry to Wasm
1262-
/// are not visible to this cursor.
1263-
///
12641259
/// Returns `None` if debug instrumentation is not enabled for
12651260
/// the engine containing this store.
12661261
#[cfg(feature = "debug")]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use wasmtime_unwinder::Handler;
3232

3333
pub use self::backtrace::Backtrace;
3434
#[cfg(feature = "debug")]
35-
pub(crate) use self::backtrace::CurrentActivationBacktrace;
35+
pub(crate) use self::backtrace::{FrameOrHostCode, StoreBacktrace};
3636
#[cfg(feature = "gc")]
3737
pub use wasmtime_unwinder::Frame;
3838

0 commit comments

Comments
 (0)