1 //! Exception-throw logic for Wasm exceptions.
2 
3 use core::ptr::NonNull;
4 
5 use wasmtime_environ::TagIndex;
6 use wasmtime_unwinder::{Frame, ThrowAction};
7 
8 use super::{VMContext, VMStore};
9 use crate::{store::AutoAssertNoGc, vm::Instance};
10 
11 /// Compute the target of the pending exception on the store.
12 ///
13 /// # Safety
14 ///
15 /// The stored last-exit state in `store` either must be valid, or
16 /// must have a zeroed exit FP if no Wasm is on the stack.
17 pub unsafe fn compute_throw_action(store: &mut dyn VMStore) -> ThrowAction {
18     let mut nogc = AutoAssertNoGc::new(store.store_opaque_mut());
19 
20     // Get the tag identity relative to the store.
21 
22     // Temporarily take, to avoid borrowing issues.
23     let exnref = nogc
24         .take_pending_exception()
25         .expect("Only invoked when an exception is pending");
26     let (throwing_tag_instance_id, throwing_tag_defined_tag_index) =
27         exnref.tag(&mut nogc).expect("cannot read tag");
28     nogc.set_pending_exception(exnref);
29     log::trace!(
30         "throwing: tag defined in instance {throwing_tag_instance_id:?} defined-tag {throwing_tag_defined_tag_index:?}"
31     );
32 
33     // Get the state needed for a stack walk.
34     let (exit_pc, exit_fp, entry_fp) = unsafe {
35         (
36             *nogc.vm_store_context().last_wasm_exit_pc.get(),
37             nogc.vm_store_context().last_wasm_exit_fp(),
38             *nogc.vm_store_context().last_wasm_entry_fp.get(),
39         )
40     };
41 
42     // Early out: if there is no exit FP -- which can happen if a host
43     // func, wrapped up as a `Func`, is called directly via
44     // `Func::call` -- then the only possible action we can take is
45     // `None` (i.e., no handler, unwind to entry from host).
46     if exit_fp == 0 {
47         return ThrowAction::None;
48     }
49 
50     // Walk the stack, looking up the module with each PC, and using
51     // that module to resolve local tag indices into (instance, tag)
52     // tuples.
53     let handler_lookup = |frame: &Frame| -> Option<(usize, usize)> {
54         log::trace!(
55             "exception-throw stack walk: frame at FP={:x} PC={:x}",
56             frame.fp(),
57             frame.pc()
58         );
59         let module = nogc.modules().lookup_module_by_pc(frame.pc())?;
60         let base = module.code_object().code_memory().text().as_ptr() as usize;
61         let rel_pc = u32::try_from(frame.pc().wrapping_sub(base)).expect("Module larger than 4GiB");
62         let et = module.exception_table();
63         let (frame_offset, handlers) = et.lookup_pc(rel_pc);
64         let fp_to_sp = frame_offset.map(|frame_offset| -isize::try_from(frame_offset).unwrap());
65         for handler in handlers {
66             log::trace!("-> checking handler: {handler:?}");
67             let is_match = match handler.tag {
68                 // Catch-all/default handler. Always come last in sequence.
69                 None => true,
70                 Some(module_local_tag_index) => {
71                     let fp_to_sp =
72                         fp_to_sp.expect("frame offset is necessary for exception unwind");
73                     let fp_offset = fp_to_sp
74                         + isize::try_from(
75                             handler
76                                 .context_sp_offset
77                                 .expect("dynamic context not present for handler record"),
78                         )
79                         .unwrap();
80                     let frame_vmctx = unsafe { frame.read_slot_from_fp(fp_offset) };
81                     log::trace!("-> read vmctx from frame: {frame_vmctx:x}");
82                     let frame_vmctx =
83                         NonNull::new(frame_vmctx as *mut VMContext).expect("null vmctx in frame");
84 
85                     // SAFETY: we use `Instance::from_vmctx` to get an
86                     // `&Instance` from a raw vmctx we read off the
87                     // stack frame. That method's safety requirements
88                     // are that the `vmctx` is a valid vmctx belonging
89                     // to an instance in the store (`nogc`). This is
90                     // satisfied because every Wasm frame in this
91                     // activation must belong to an instance in this
92                     // store: we do not permit cross-store calls
93                     // without exiting through host code.
94                     let (handler_tag_instance, handler_tag_index) = unsafe {
95                         let store_id = nogc.id();
96                         let instance = Instance::from_vmctx(&nogc, frame_vmctx);
97                         let tag = instance
98                             .get_exported_tag(store_id, TagIndex::from_u32(module_local_tag_index));
99                         tag.to_raw_indices()
100                     };
101                     log::trace!(
102                         "-> handler's tag {module_local_tag_index:?} resolves to instance {handler_tag_instance:?} defined-tag {handler_tag_index:?}"
103                     );
104 
105                     handler_tag_instance == throwing_tag_instance_id
106                         && handler_tag_index == throwing_tag_defined_tag_index
107                 }
108             };
109             if is_match {
110                 let fp_to_sp = fp_to_sp.expect("frame offset must be known if we found a handler");
111                 return Some((
112                     base.wrapping_add(
113                         usize::try_from(handler.handler_offset).expect("Module larger than usize"),
114                     ),
115                     frame.fp().wrapping_add_signed(fp_to_sp),
116                 ));
117             }
118         }
119         None
120     };
121     let unwinder = nogc.unwinder();
122     let action = unsafe {
123         wasmtime_unwinder::compute_throw_action(
124             unwinder,
125             handler_lookup,
126             exit_pc,
127             exit_fp,
128             entry_fp,
129         )
130     };
131 
132     log::trace!("throw action: {action:?}");
133     action
134 }
135