wasmer_vm/libcalls/eh/
gcc.rs

1//! Implementation of personality function and unwinding support for Wasmer.
2//!
3//! On a native platform, when an exception is thrown, the type info of the
4//! exception is known, and can be matched against the LSDA table within the
5//! personality function (e.g. __gxx_personality_v0 for Itanium ABI).
6//!
7//! However, in WASM, the exception "type" can change between compilation
8//! and instantiation because tags can be imported from other modules. Also,
9//! a single module can be instantiated many times, but all instances share
10//! the same code, differing only in their VMContext data. This means that,
11//! to be able to match the thrown exception against the expected tag in
12//! catch clauses, we need to go through the VMContext of the specific instance
13//! to which the stack frame belongs; nothing else can tell us exactly which
14//! instance we're currently looking at, including the IP which will be the
15//! same for all instances of the same module.
16//!
17//! To achieve this, we use a two-stage personality function. The first stage
18//! is the normal personality function which is called by libunwind; this
19//! function always catches the exception as long as it's a Wasmer exception,
20//! without looking at the specific tags. Afterwards, control is transferred
21//! to the module's landing pad, which can load its VMContext and pass it to
22//! the second stage of the personality function. Afterwards, the second stage
23//! can take the "local tag number" (the tag index as seen from the WASM
24//! module's point of view) from the LSDA and translate it to the unique tag
25//! within the Store, and match that against the thrown exception's tag.
26//!
27//! The throw function also uses the VMContext of its own instance to get the
28//! unique tag from the Store, and uses that as the final exception tag.
29//!
30//! It's important to note that we can't count on libunwind behaving properly
31//! if we make calls from the second stage of the personality function; this is
32//! why the first stage has to extract all the data necessary for the second
33//! stage and place it in the exception object. The second stage will clear
34//! out the data before returning, so further stack frames will not get stale
35//! data by mistake.
36
37use std::ffi::c_void;
38
39use libunwind::{self as uw};
40use wasmer_types::TagIndex;
41
42use crate::{InternalStoreHandle, StoreHandle, StoreObjects, VMContext, VMExceptionRef};
43
44use super::dwarf::eh::{self, EHAction, EHContext};
45
46// In case where multiple copies of std exist in a single process,
47// we use address of this static variable to distinguish an exception raised by
48// this copy and some other copy (which needs to be treated as foreign exception).
49static CANARY: u8 = 0;
50const WASMER_EXCEPTION_CLASS: uw::_Unwind_Exception_Class = u64::from_ne_bytes(*b"WMERWASM");
51
52const CATCH_ALL_TAG_VALUE: i32 = i32::MAX;
53// This constant is not reflected in the generated code, but the switch block
54// has a default action of rethrowing the exception, which this value should
55// trigger.
56const NO_MATCH_FOUND_TAG_VALUE: i32 = i32::MAX - 1;
57
58#[repr(C)]
59pub struct UwExceptionWrapper {
60    pub _uwe: uw::_Unwind_Exception,
61    pub canary: *const u8,
62    pub exnref: u32,
63
64    // First stage -> second stage communication
65    pub current_frame_info: Option<Box<CurrentFrameInfo>>,
66}
67
68#[repr(C)]
69pub struct CurrentFrameInfo {
70    pub catch_tags: Vec<u32>,
71    pub has_catch_all: bool,
72}
73
74impl UwExceptionWrapper {
75    pub fn new(exnref: u32) -> Self {
76        Self {
77            _uwe: uw::_Unwind_Exception {
78                exception_class: WASMER_EXCEPTION_CLASS,
79                exception_cleanup: Some(deallocate_exception),
80                private_1: core::ptr::null::<u8>() as usize as _,
81                private_2: 0,
82            },
83            canary: &CANARY,
84            exnref,
85            current_frame_info: None,
86        }
87    }
88}
89
90#[cfg(target_arch = "x86_64")]
91const UNWIND_DATA_REG: (i32, i32) = (0, 1); // RAX, RDX
92
93#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
94const UNWIND_DATA_REG: (i32, i32) = (0, 1); // R0, R1 / X0, X1
95
96#[cfg(any(target_arch = "riscv64", target_arch = "riscv32"))]
97const UNWIND_DATA_REG: (i32, i32) = (10, 11); // x10, x11
98
99#[cfg(target_arch = "loongarch64")]
100const UNWIND_DATA_REG: (i32, i32) = (4, 5); // a0, a1
101
102#[unsafe(no_mangle)]
103/// The implementation of Wasmer's personality function.
104///
105/// # Safety
106///
107/// Performs libunwind unwinding magic.
108pub unsafe extern "C" fn wasmer_eh_personality(
109    version: std::ffi::c_int,
110    actions: uw::_Unwind_Action,
111    exception_class: uw::_Unwind_Exception_Class,
112    exception_object: *mut uw::_Unwind_Exception,
113    context: *mut uw::_Unwind_Context,
114) -> uw::_Unwind_Reason_Code {
115    unsafe {
116        if version != 1 {
117            return uw::_Unwind_Reason_Code__URC_FATAL_PHASE1_ERROR;
118        }
119
120        let uw_exc = std::mem::transmute::<*mut uw::_Unwind_Exception, *mut UwExceptionWrapper>(
121            exception_object,
122        );
123
124        if exception_class != WASMER_EXCEPTION_CLASS {
125            return uw::_Unwind_Reason_Code__URC_CONTINUE_UNWIND;
126        }
127
128        let eh_action = match find_eh_action(context) {
129            Ok(action) => action,
130            Err(_) => {
131                return uw::_Unwind_Reason_Code__URC_FATAL_PHASE1_ERROR;
132            }
133        };
134
135        if actions as i32 & uw::_Unwind_Action__UA_SEARCH_PHASE as i32 != 0 {
136            match eh_action {
137                EHAction::None => uw::_Unwind_Reason_Code__URC_CONTINUE_UNWIND,
138                EHAction::CatchAll { .. }
139                | EHAction::CatchSpecific { .. }
140                | EHAction::CatchSpecificOrAll { .. } => uw::_Unwind_Reason_Code__URC_HANDLER_FOUND,
141                EHAction::Terminate => uw::_Unwind_Reason_Code__URC_FATAL_PHASE1_ERROR,
142            }
143        } else {
144            // For the catch-specific vs catch-specific-or-all case below, checked before
145            // we move eh_action out in the match
146            let has_catch_all = matches!(eh_action, EHAction::CatchSpecificOrAll { .. });
147
148            match eh_action {
149                EHAction::None => uw::_Unwind_Reason_Code__URC_CONTINUE_UNWIND,
150                EHAction::CatchAll { lpad } => {
151                    uw::_Unwind_SetGR(context, UNWIND_DATA_REG.0, uw_exc as _);
152                    // Zero means immediate catch-all
153                    uw::_Unwind_SetGR(context, UNWIND_DATA_REG.1, 0);
154                    uw::_Unwind_SetIP(context, lpad as usize as _);
155                    uw::_Unwind_Reason_Code__URC_INSTALL_CONTEXT
156                }
157                EHAction::CatchSpecific { lpad, tags }
158                | EHAction::CatchSpecificOrAll { lpad, tags } => {
159                    (*uw_exc).current_frame_info = Some(Box::new(CurrentFrameInfo {
160                        catch_tags: tags,
161                        has_catch_all,
162                    }));
163                    uw::_Unwind_SetGR(context, UNWIND_DATA_REG.0, uw_exc as _);
164                    // One means enter phase 2
165                    uw::_Unwind_SetGR(context, UNWIND_DATA_REG.1, 1);
166                    uw::_Unwind_SetIP(context, lpad as usize as _);
167                    uw::_Unwind_Reason_Code__URC_INSTALL_CONTEXT
168                }
169                EHAction::Terminate => uw::_Unwind_Reason_Code__URC_FATAL_PHASE2_ERROR,
170            }
171        }
172    }
173}
174
175#[unsafe(no_mangle)]
176/// The second stage of the personality function. See module level documentation
177/// for an explanation of the exact procedure used during unwinding.
178///
179/// # Safety
180///
181/// Does pointer accesses, which must be valid.
182pub unsafe extern "C" fn wasmer_eh_personality2(
183    vmctx: *mut VMContext,
184    exception_object: *mut UwExceptionWrapper,
185) -> i32 {
186    unsafe {
187        let Some(current_frame_info) = (*exception_object).current_frame_info.take() else {
188            // This should never happen
189            unreachable!("wasmer_eh_personality2 called without current_frame_info");
190        };
191
192        let instance = (*vmctx).instance();
193        let exn = super::exn_obj_from_exnref(vmctx, (*exception_object).exnref);
194
195        for tag in current_frame_info.catch_tags {
196            let unique_tag = instance.shared_tag_ptr(TagIndex::from_u32(tag)).index();
197            if unique_tag == (*exn).tag_index() {
198                return tag as i32;
199            }
200        }
201
202        if current_frame_info.has_catch_all {
203            CATCH_ALL_TAG_VALUE
204        } else {
205            NO_MATCH_FOUND_TAG_VALUE
206        }
207    }
208}
209
210unsafe fn find_eh_action(context: *mut uw::_Unwind_Context) -> Result<EHAction, ()> {
211    unsafe {
212        let lsda = uw::_Unwind_GetLanguageSpecificData(context) as *const u8;
213        let mut ip_before_instr: std::ffi::c_int = 0;
214        let ip = uw::_Unwind_GetIPInfo(context, &mut ip_before_instr);
215        let eh_context = EHContext {
216            // The return address points 1 byte past the call instruction,
217            // which could be in the next IP range in LSDA range table.
218            //
219            // `ip = -1` has special meaning, so use wrapping sub to allow for that
220            ip: if ip_before_instr != 0 {
221                ip as _
222            } else {
223                ip.wrapping_sub(1) as _
224            },
225            func_start: uw::_Unwind_GetRegionStart(context) as *const _,
226            get_text_start: &|| uw::_Unwind_GetTextRelBase(context) as *const _,
227            get_data_start: &|| uw::_Unwind_GetDataRelBase(context) as *const _,
228        };
229        eh::find_eh_action(lsda, &eh_context)
230    }
231}
232
233pub unsafe fn read_exnref(exception: *mut c_void) -> u32 {
234    if exception.is_null() {
235        0
236    } else {
237        unsafe { (*(exception as *mut UwExceptionWrapper)).exnref }
238    }
239}
240
241/// # Safety
242///
243/// Performs libunwind unwinding magic. Highly unsafe.
244pub unsafe fn throw(ctx: &StoreObjects, exnref: u32) -> ! {
245    unsafe {
246        if exnref == 0 {
247            crate::raise_lib_trap(crate::Trap::lib(
248                wasmer_types::TrapCode::UninitializedExnRef,
249            ))
250        }
251
252        let exception = Box::new(UwExceptionWrapper::new(exnref));
253        let exception_ptr = Box::into_raw(exception);
254
255        match uw::_Unwind_RaiseException(exception_ptr as *mut libunwind::_Unwind_Exception) {
256            libunwind::_Unwind_Reason_Code__URC_END_OF_STACK => {
257                delete_exception(exception_ptr as *mut c_void);
258
259                let exnref = VMExceptionRef(StoreHandle::from_internal(
260                    ctx.id(),
261                    InternalStoreHandle::from_index(exnref as usize).unwrap(),
262                ));
263                crate::raise_lib_trap(crate::Trap::uncaught_exception(exnref, ctx))
264            }
265            _ => {
266                unreachable!()
267            }
268        }
269    }
270}
271
272pub unsafe fn delete_exception(exception: *mut c_void) {
273    unsafe {
274        if !exception.is_null() {
275            uw::_Unwind_DeleteException(exception as *mut uw::_Unwind_Exception);
276        }
277    }
278}
279
280unsafe extern "C" fn deallocate_exception(
281    _: uw::_Unwind_Reason_Code,
282    exception: *mut uw::_Unwind_Exception,
283) {
284    unsafe {
285        let exception = Box::from_raw(exception as *mut UwExceptionWrapper);
286        drop(exception);
287    }
288}