Skip to content

Commit 1f9f65a

Browse files
committed
Refactor exception handling to do a separate unwind, like traps. Support host-to-Wasm, Wasm-to-host, and host-to-host exceptions.
1 parent a076222 commit 1f9f65a

File tree

14 files changed

+466
-141
lines changed

14 files changed

+466
-141
lines changed

crates/cranelift/src/compiler.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1374,10 +1374,21 @@ fn save_last_wasm_exit_fp_and_pc(
13741374
ptr: &impl PtrSize,
13751375
limits: Value,
13761376
) {
1377+
// Save the trampoline FP to the limits. Exception unwind needs
1378+
// this so that it can know the SP (bottom of frame) for the very
1379+
// last Wasm frame.
1380+
let trampoline_fp = builder.ins().get_frame_pointer(pointer_type);
1381+
builder.ins().store(
1382+
MemFlags::trusted(),
1383+
trampoline_fp,
1384+
limits,
1385+
ptr.vmstore_context_last_wasm_exit_trampoline_fp(),
1386+
);
1387+
13771388
// Save the exit Wasm FP to the limits. We dereference the current FP to get
13781389
// the previous FP because the current FP is the trampoline's FP, and we
13791390
// want the Wasm function's FP, which is the caller of this trampoline.
1380-
let trampoline_fp = builder.ins().get_frame_pointer(pointer_type);
1391+
13811392
let wasm_fp = builder.ins().load(
13821393
pointer_type,
13831394
MemFlags::trusted(),

crates/environ/src/trap_encoding.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,6 @@ pub enum Trap {
107107
/// that all host tasks have completed and any/all host-owned stream/future
108108
/// handles have been dropped.
109109
AsyncDeadlock,
110-
111-
/// An exception was thrown out of Wasm into the host and this is
112-
/// not supported in the current configuration.
113-
ExceptionToHost,
114110
// if adding a variant here be sure to update the `check!` macro below
115111
}
116112

@@ -152,7 +148,6 @@ impl Trap {
152148
ContinuationAlreadyConsumed
153149
DisabledOpcode
154150
AsyncDeadlock
155-
ExceptionToHost
156151
}
157152

158153
None
@@ -188,7 +183,6 @@ impl fmt::Display for Trap {
188183
ContinuationAlreadyConsumed => "continuation already consumed",
189184
DisabledOpcode => "pulley opcode disabled at compile time was executed",
190185
AsyncDeadlock => "deadlock detected: event loop cannot make further progress",
191-
ExceptionToHost => "Wasm exception thrown across host boundary",
192186
};
193187
write!(f, "wasm trap: {desc}")
194188
}

crates/environ/src/vmoffsets.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,26 @@ pub trait PtrSize {
190190
/// Return the offset of the `gc_heap.base` field within a `VMStoreContext`.
191191
fn vmstore_context_gc_heap_base(&self) -> u8 {
192192
let offset = self.vmstore_context_gc_heap() + self.vmmemory_definition_base();
193-
debug_assert!(offset < self.vmstore_context_last_wasm_exit_fp());
193+
debug_assert!(offset < self.vmstore_context_last_wasm_exit_trampoline_fp());
194194
offset
195195
}
196196

197197
/// Return the offset of the `gc_heap.current_length` field within a `VMStoreContext`.
198198
fn vmstore_context_gc_heap_current_length(&self) -> u8 {
199199
let offset = self.vmstore_context_gc_heap() + self.vmmemory_definition_current_length();
200-
debug_assert!(offset < self.vmstore_context_last_wasm_exit_fp());
200+
debug_assert!(offset < self.vmstore_context_last_wasm_exit_trampoline_fp());
201201
offset
202202
}
203203

204+
/// Return the offset of the `last_wasm_exit_trampoline_fp` field
205+
/// of `VMStoreContext`.
206+
fn vmstore_context_last_wasm_exit_trampoline_fp(&self) -> u8 {
207+
self.vmstore_context_gc_heap() + self.size_of_vmmemory_definition()
208+
}
209+
204210
/// Return the offset of the `last_wasm_exit_fp` field of `VMStoreContext`.
205211
fn vmstore_context_last_wasm_exit_fp(&self) -> u8 {
206-
self.vmstore_context_gc_heap() + self.size_of_vmmemory_definition()
212+
self.vmstore_context_last_wasm_exit_trampoline_fp() + self.size()
207213
}
208214

209215
/// Return the offset of the `last_wasm_exit_pc` field of `VMStoreContext`.

crates/unwinder/src/throw.rs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,39 @@ pub unsafe fn compute_throw_action<F: Fn(&Frame) -> Option<usize>>(
5050
unwind: &dyn Unwind,
5151
frame_handler: F,
5252
exit_pc: usize,
53-
exit_frame: usize,
53+
exit_trampoline_frame: usize,
5454
entry_frame: usize,
5555
) -> ThrowAction {
5656
// SAFETY: the safety of `visit_frames` relies on the correctness of the
5757
// parameters passed in which is forwarded as a contract to this function
5858
// tiself.
5959
let result = unsafe {
60-
crate::stackwalk::visit_frames(unwind, exit_pc, exit_frame, entry_frame, |frame| {
61-
let Some(sp) = frame.sp() else {
62-
// Cannot possibly unwind to this frame if SP is not
63-
// known. This is only the case for the first
64-
// (trampoline) frame; after that, we know SP at the
65-
// callsite because we know the offset from the lower
66-
// FP to the next frame's SP.
67-
return ControlFlow::Continue(());
68-
};
60+
crate::stackwalk::visit_frames(
61+
unwind,
62+
exit_pc,
63+
exit_trampoline_frame,
64+
entry_frame,
65+
|frame| {
66+
log::trace!("visit_frame: frame {frame:?}");
67+
let Some(sp) = frame.sp() else {
68+
// Cannot possibly unwind to this frame if SP is not
69+
// known. This is only the case for the first
70+
// (trampoline) frame; after that, we know SP at the
71+
// callsite because we know the offset from the lower
72+
// FP to the next frame's SP.
73+
return ControlFlow::Continue(());
74+
};
6975

70-
if let Some(handler_pc) = frame_handler(&frame) {
71-
return ControlFlow::Break(ThrowAction::Handler {
72-
pc: handler_pc,
73-
sp,
74-
fp: frame.fp(),
75-
});
76-
}
77-
ControlFlow::Continue(())
78-
})
76+
if let Some(handler_pc) = frame_handler(&frame) {
77+
return ControlFlow::Break(ThrowAction::Handler {
78+
pc: handler_pc,
79+
sp,
80+
fp: frame.fp(),
81+
});
82+
}
83+
ControlFlow::Continue(())
84+
},
85+
)
7986
};
8087
match result {
8188
ControlFlow::Break(action) => action,

crates/wasmtime/src/runtime/func.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
use crate::prelude::*;
21
use crate::runtime::Uninhabited;
32
use crate::runtime::vm::{
43
InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation,
54
VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, VMStoreContext,
65
};
76
use crate::store::{AutoAssertNoGc, StoreId, StoreOpaque};
87
use crate::type_registry::RegisteredType;
8+
use crate::vm::{ExceptionTombstone, VMGcRef};
99
use crate::{
1010
AsContext, AsContextMut, CallHook, Engine, Extern, FuncType, Instance, ModuleExport, Ref,
1111
StoreContext, StoreContextMut, Val, ValRaw, ValType,
1212
};
13+
use crate::{ExnRef, Rooted, prelude::*};
1314
use alloc::sync::Arc;
1415
use core::ffi::c_void;
1516
#[cfg(feature = "async")]
@@ -1265,7 +1266,20 @@ impl Func {
12651266

12661267
val_vec.extend((0..ty.results().len()).map(|_| Val::null_func_ref()));
12671268
let (params, results) = val_vec.split_at_mut(nparams);
1268-
func(caller.sub_caller(), params, results)?;
1269+
match func(caller.sub_caller(), params, results) {
1270+
Ok(()) => {}
1271+
Err(e) if e.is::<Rooted<ExnRef>>() => {
1272+
let exnref = e.downcast::<Rooted<ExnRef>>().unwrap();
1273+
let exnref = VMGcRef::from_raw_u32(exnref.to_raw(&mut caller.store.0).unwrap())
1274+
.unwrap()
1275+
.into_exnref_unchecked();
1276+
caller.store.0.set_pending_exception(exnref);
1277+
return Err(ExceptionTombstone.into());
1278+
}
1279+
Err(e) => {
1280+
return Err(e);
1281+
}
1282+
}
12691283

12701284
// Unlike our arguments we need to dynamically check that the return
12711285
// values produced are correct. There could be a bug in `func` that
@@ -1513,7 +1527,7 @@ pub(crate) fn invoke_wasm_and_catch_traps<T>(
15131527
let result = crate::runtime::vm::catch_traps(store, &mut previous_runtime_state, closure);
15141528
core::mem::drop(previous_runtime_state);
15151529
store.0.call_hook(CallHook::ReturningFromWasm)?;
1516-
result.map_err(|t| crate::trap::from_runtime_box(store.0, t))
1530+
result
15171531
}
15181532
}
15191533

@@ -1532,6 +1546,9 @@ pub(crate) struct EntryStoreContext {
15321546
/// Contains value of `last_wasm_exit_fp` field to restore in
15331547
/// `VMStoreContext` when exiting Wasm.
15341548
pub last_wasm_exit_fp: usize,
1549+
/// Contains value of `last_wasm_exit_trampoline_fp` field to restore in
1550+
/// `VMStoreContext` when exiting Wasm.
1551+
pub last_wasm_exit_trampoline_fp: usize,
15351552
/// Contains value of `last_wasm_entry_fp` field to restore in
15361553
/// `VMStoreContext` when exiting Wasm.
15371554
pub last_wasm_entry_fp: usize,
@@ -1625,6 +1642,11 @@ impl EntryStoreContext {
16251642
unsafe {
16261643
let last_wasm_exit_pc = *store.0.vm_store_context().last_wasm_exit_pc.get();
16271644
let last_wasm_exit_fp = *store.0.vm_store_context().last_wasm_exit_fp.get();
1645+
let last_wasm_exit_trampoline_fp = *store
1646+
.0
1647+
.vm_store_context()
1648+
.last_wasm_exit_trampoline_fp
1649+
.get();
16281650
let last_wasm_entry_fp = *store.0.vm_store_context().last_wasm_entry_fp.get();
16291651

16301652
let stack_chain = (*store.0.vm_store_context().stack_chain.get()).clone();
@@ -1638,6 +1660,7 @@ impl EntryStoreContext {
16381660
stack_limit,
16391661
last_wasm_exit_pc,
16401662
last_wasm_exit_fp,
1663+
last_wasm_exit_trampoline_fp,
16411664
last_wasm_entry_fp,
16421665
stack_chain,
16431666
vm_store_context,
@@ -1657,6 +1680,8 @@ impl EntryStoreContext {
16571680
}
16581681

16591682
*(*self.vm_store_context).last_wasm_exit_fp.get() = self.last_wasm_exit_fp;
1683+
*(*self.vm_store_context).last_wasm_exit_trampoline_fp.get() =
1684+
self.last_wasm_exit_trampoline_fp;
16601685
*(*self.vm_store_context).last_wasm_exit_pc.get() = self.last_wasm_exit_pc;
16611686
*(*self.vm_store_context).last_wasm_entry_fp.get() = self.last_wasm_entry_fp;
16621687
*(*self.vm_store_context).stack_chain.get() = self.stack_chain.clone();

crates/wasmtime/src/runtime/gc/enabled/exnref.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,15 @@ impl ExnRef {
630630
}
631631
}
632632

633+
/// Rooted<ExnRef> implements Error so that it can be boxed and
634+
/// returned in an `anyhow::Result` from host-to-Wasm function calls.
635+
impl core::error::Error for Rooted<ExnRef> {}
636+
impl core::fmt::Display for Rooted<ExnRef> {
637+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
638+
write!(f, "Wasm exception")
639+
}
640+
}
641+
633642
unsafe impl WasmTy for Rooted<ExnRef> {
634643
#[inline]
635644
fn valtype() -> ValType {

crates/wasmtime/src/runtime/store.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ use crate::prelude::*;
8989
use crate::runtime::vm::GcRootsList;
9090
#[cfg(feature = "stack-switching")]
9191
use crate::runtime::vm::VMContRef;
92-
#[cfg(feature = "gc")]
93-
use crate::runtime::vm::VMExnRef;
9492
use crate::runtime::vm::mpk::ProtectionKey;
9593
use crate::runtime::vm::{
9694
self, GcStore, Imports, InstanceAllocationRequest, InstanceAllocator, InstanceHandle,
@@ -124,6 +122,8 @@ pub(crate) use token::StoreToken;
124122
mod async_;
125123
#[cfg(all(feature = "async", feature = "call-hook"))]
126124
pub use self::async_::CallHookHandler;
125+
126+
use super::vm::VMExnRef;
127127
#[cfg(feature = "gc")]
128128
mod gc;
129129

@@ -357,6 +357,12 @@ pub struct StoreOpaque {
357357
// Types for which the embedder has created an allocator for.
358358
#[cfg(feature = "gc")]
359359
gc_host_alloc_types: crate::hash_set::HashSet<crate::type_registry::RegisteredType>,
360+
/// Pending exception, if any. This is also a GC root, because it
361+
/// needs to be rooted somewhere between the time that a pending
362+
/// exception is set and the time that the Wasm-to-host entry
363+
/// trampoline starts the unwind (right before it would return).
364+
#[cfg(feature = "gc")]
365+
pending_exception: Option<VMGcRef>,
360366

361367
// Numbers of resources instantiated in this store, and their limits
362368
instance_count: usize,
@@ -575,6 +581,8 @@ impl<T> Store<T> {
575581
gc_roots_list: GcRootsList::default(),
576582
#[cfg(feature = "gc")]
577583
gc_host_alloc_types: Default::default(),
584+
#[cfg(feature = "gc")]
585+
pending_exception: None,
578586
modules: ModuleRegistry::default(),
579587
func_refs: FuncRefs::default(),
580588
host_globals: PrimaryMap::new(),
@@ -1659,6 +1667,7 @@ impl StoreOpaque {
16591667
self.trace_wasm_continuation_roots(gc_roots_list);
16601668
self.trace_vmctx_roots(gc_roots_list);
16611669
self.trace_user_roots(gc_roots_list);
1670+
self.trace_pending_exception_roots(gc_roots_list);
16621671

16631672
log::trace!("End trace GC roots")
16641673
}
@@ -1780,6 +1789,17 @@ impl StoreOpaque {
17801789
log::trace!("End trace GC roots :: user");
17811790
}
17821791

1792+
#[cfg(feature = "gc")]
1793+
fn trace_pending_exception_roots(&mut self, gc_roots_list: &mut GcRootsList) {
1794+
log::trace!("Begin trace GC roots :: pending exception");
1795+
if let Some(pending_exception) = self.pending_exception.as_mut() {
1796+
unsafe {
1797+
gc_roots_list.add_root(pending_exception.into(), "Pending exception");
1798+
}
1799+
}
1800+
log::trace!("End trace GC roots :: pending exception");
1801+
}
1802+
17831803
/// Insert a host-allocated GC type into this store.
17841804
///
17851805
/// This makes it suitable for the embedder to allocate instances of this
@@ -2144,18 +2164,6 @@ at https://bytecodealliance.org/security.
21442164
Ok(ptr)
21452165
}
21462166

2147-
/// Throws an exception.
2148-
///
2149-
/// # Safety
2150-
///
2151-
/// Must be invoked when Wasm code is on the stack.
2152-
#[cfg(feature = "gc")]
2153-
pub(crate) unsafe fn throw_ref(&mut self, exnref: VMExnRef) -> ! {
2154-
let mut nogc = AutoAssertNoGc::new(self);
2155-
// SAFETY: this is invoked when Wasm is on the stack.
2156-
unsafe { vm::throw(&mut nogc, exnref) }
2157-
}
2158-
21592167
/// Constructs and executes an `InstanceAllocationRequest` and pushes the
21602168
/// returned instance into the store.
21612169
///
@@ -2231,6 +2239,24 @@ at https://bytecodealliance.org/security.
22312239

22322240
Ok(id)
22332241
}
2242+
2243+
/// Set a pending exception. The `exnref` is taken and held on
2244+
/// this store to be fetched later by an unwind. This method does
2245+
/// *not* set up an unwind request on the TLS call state; that
2246+
/// must be done separately.
2247+
#[cfg(feature = "gc")]
2248+
pub(crate) fn set_pending_exception(&mut self, exnref: VMExnRef) {
2249+
self.pending_exception = Some(exnref.into());
2250+
}
2251+
2252+
/// Take a pending exception.
2253+
#[cfg(feature = "gc")]
2254+
pub(crate) fn take_pending_exception(&mut self) -> VMExnRef {
2255+
self.pending_exception
2256+
.take()
2257+
.expect("No pending exception set")
2258+
.into_exnref_unchecked()
2259+
}
22342260
}
22352261

22362262
/// Helper parameter to [`StoreOpaque::allocate_instance`].

crates/wasmtime/src/runtime/store/async_.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ impl StoreOpaque {
230230
self.trace_vmctx_roots(gc_roots_list);
231231
Yield::new().await;
232232
self.trace_user_roots(gc_roots_list);
233+
Yield::new().await;
234+
self.trace_pending_exception_roots(gc_roots_list);
233235

234236
log::trace!("End trace GC roots")
235237
}

crates/wasmtime/src/runtime/trap.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use super::ExnRef;
12
#[cfg(feature = "coredump")]
23
use super::coredump::WasmCoreDump;
4+
use super::store::AutoAssertNoGc;
5+
use super::vm::ExceptionTombstone;
36
use crate::prelude::*;
47
use crate::store::StoreOpaque;
58
use crate::{AsContext, Module};
@@ -79,6 +82,13 @@ pub(crate) fn from_runtime_box(
7982
coredumpstack,
8083
} = *runtime_trap;
8184
let (mut error, pc) = match reason {
85+
crate::runtime::vm::TrapReason::User(error) if error.is::<ExceptionTombstone>() => {
86+
let mut nogc = AutoAssertNoGc::new(store);
87+
let exnref = nogc.take_pending_exception();
88+
let exnref = ExnRef::_from_raw(&mut nogc, exnref.as_gc_ref().as_raw_u32())
89+
.expect("Exception should be non-null");
90+
(exnref.into(), None)
91+
}
8292
// For user-defined errors they're already an `anyhow::Error` so no
8393
// conversion is really necessary here, but a `backtrace` may have
8494
// been captured so it's attempted to get inserted here.

0 commit comments

Comments
 (0)