Next Generation WASM Microkernel Operating System
1// Copyright 2025 Jonas Kruckenberg
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8#![no_std] // this is crate is fully incompatible with `std` due to clashing lang item definitions
9#![cfg(target_os = "none")]
10#![expect(internal_features, reason = "lang items")]
11#![feature(
12 core_intrinsics,
13 rustc_attrs,
14 used_with_arg,
15 lang_items,
16 naked_functions,
17 never_type
18)]
19
20extern crate alloc;
21
22mod arch;
23mod eh_action;
24mod eh_info;
25mod error;
26mod exception;
27mod frame;
28mod lang_items;
29mod utils;
30
31use abort::abort;
32use alloc::boxed::Box;
33use core::any::Any;
34use core::intrinsics;
35use core::mem::ManuallyDrop;
36use core::panic::UnwindSafe;
37use core::ptr::addr_of_mut;
38use eh_action::{EHAction, find_eh_action};
39pub use eh_info::EhInfo;
40pub use error::Error;
41use exception::Exception;
42use fallible_iterator::FallibleIterator;
43pub use frame::{Frame, FrameIter};
44use lang_items::ensure_personality_stub;
45pub use utils::with_context;
46
47pub use arch::Registers;
48
49pub(crate) type Result<T> = core::result::Result<T, Error>;
50
51/// Begin unwinding the stack.
52///
53/// Unwinding will walk up the stack, calling [`Drop`] handlers along the way to perform cleanup until
54/// it reaches a [`catch_unwind`] handler.
55///
56/// When reached, control is transferred to the [`catch_unwind`] handler with the `payload` argument
57/// returned in the `Err` variant of the [`catch_unwind`] return. In that case, this function will *not*
58/// return.
59///
60/// # Errors
61///
62/// If there is no [`catch_unwind`] handler anywhere in the call chain then this function returns
63/// `Err(Error::EndOfStack)`. This roughly equivalent to an uncaught exception in C++ and should
64/// be treated as a fatal error.
65pub fn begin_unwind(payload: Box<dyn Any + Send>) -> Result<!> {
66 with_context(|regs, pc| {
67 let frames = FrameIter::from_registers(regs.clone(), pc);
68
69 raise_exception_phase2(frames, Exception::wrap(payload))
70 })
71}
72
73/// Begin unwinding *a* stack. The specific stack location at which unwinding will begin is determined
74/// by the register set and program counter provided.
75///
76/// Unwinding will walk up the stack, calling [`Drop`] handlers along the way to perform cleanup until
77/// it reaches a [`catch_unwind`] handler.
78///
79/// When reached, control is transferred to the [`catch_unwind`] handler with the `payload` argument
80/// returned in the `Err` variant of the [`catch_unwind`] return. In that case, this function will *not*
81/// return.
82///
83/// # Errors
84///
85/// If there is no [`catch_unwind`] handler anywhere in the call chain then this function returns
86/// `Err(Error::EndOfStack)`. This roughly equivalent to an uncaught exception in C++ and should
87/// be treated as a fatal error.
88///
89/// # Safety
90///
91/// This function does not perform any checking of the provided register values, if they are incorrect
92/// this might lead to segfaults.
93pub unsafe fn begin_unwind_with(
94 payload: Box<dyn Any + Send>,
95 regs: Registers,
96 pc: usize,
97) -> Result<!> {
98 let frames = FrameIter::from_registers(regs, pc);
99
100 raise_exception_phase2(frames, Exception::wrap(payload))
101}
102
103/// Walk up the stack until either a landing pad is encountered or we reach the end of the stack.
104///
105/// If a landing pad is found control is transferred to it and this function will not return, if there
106/// is no landing pad, this function will return `Err(Error::EndOfStack)`.
107///
108/// Note that the traditional unwinding process has 2 phases, the first where the landing pad is discovered
109/// and the second where the stack is actually unwound up to that landing pad.
110/// In `unwind2` we can get away with one phase because we bypass the language personality routine:
111/// Traditional unwinders call the personality routine on each frame to discover a landing pad, and
112/// then during cleanup call the personality routine again to determine if control should actually be
113/// transferred. This is done so that languages have maximum flexibility in how they treat exceptions.
114///
115/// `unwind2` - being Rust-only - doesn't need that flexibility since Rust landing pads are called
116/// unconditionally. Furthermore, `unwind2` never actually calls the personality routine, instead
117/// parsing the [`EHAction`] for each frame directly.
118///
119/// The name `raise_exception_phase2` is kept though to make it easier to understand what this function
120/// does when coming from traditional unwinders.
121fn raise_exception_phase2(mut frames: FrameIter, exception: *mut Exception) -> Result<!> {
122 while let Some(mut frame) = frames.next()? {
123 if frame
124 .personality()
125 .map(ensure_personality_stub)
126 .transpose()?
127 .is_none()
128 {
129 continue;
130 }
131
132 let Some(mut lsda) = frame.language_specific_data() else {
133 continue;
134 };
135
136 let eh_action = find_eh_action(&mut lsda, &frame)?;
137
138 match eh_action {
139 EHAction::None => continue,
140 // Safety: As long as the Rust compiler works correctly lpad is the correct instruction
141 // pointer.
142 EHAction::Cleanup(lpad) | EHAction::Catch(lpad) => {
143 frame.set_reg(arch::UNWIND_DATA_REG.0, exception as usize);
144 frame.set_reg(arch::UNWIND_DATA_REG.1, 0);
145 frame.set_reg(arch::RA, usize::try_from(lpad).unwrap());
146 frame.adjust_stack_for_args();
147
148 // Safety: this will set up the frame context necessary to transfer control to the
149 // landing pad. Since that landing pad is generated by the Rust compiler there isn't
150 // much we can do except hope and pray that the instruction pointer is correct.
151 unsafe { frame.restore() }
152 }
153 }
154 }
155
156 Err(Error::EndOfStack)
157}
158
159/// Invokes the closure, capturing an unwind if one occurs.
160///
161/// This function returns `Ok` if no unwind occurred or `Err` with the payload passed to [`begin_unwind`].
162///
163/// You can think of this function as a `try-catch` expression and [`begin_unwind`] as the `throw`
164/// counterpart.
165///
166/// The closure provided is required to adhere to the [`UnwindSafe`] trait to ensure that all captured
167/// variables are safe to cross this boundary. The purpose of this bound is to encode the concept
168/// of [exception safety] in the type system. Most usage of this function should not need to worry about
169/// this bound as programs are naturally unwind safe without unsafe code. If it becomes a problem the
170/// [`AssertUnwindSafe`] wrapper struct can be used to quickly assert that the usage here is indeed
171/// unwind safe.
172///
173/// # Errors
174///
175/// Returns an error with the boxed panic payload when the provided closure panicked.
176///
177/// [exception safety]: https://github.com/rust-lang/rfcs/blob/master/text/1236-stabilize-catch-panic.md
178/// [`UnwindSafe`]: core::panic::UnwindSafe
179/// [`AssertUnwindSafe`]: core::panic::AssertUnwindSafe
180pub fn catch_unwind<F, R>(f: F) -> core::result::Result<R, Box<dyn Any + Send + 'static>>
181where
182 F: FnOnce() -> R + UnwindSafe,
183{
184 union Data<F, R> {
185 // when we start, this field holds the closure
186 f: ManuallyDrop<F>,
187 // when the closure completed successfully, this will hold the return
188 r: ManuallyDrop<R>,
189 // when the closure panicked this will hold the panic payload
190 p: ManuallyDrop<Box<dyn Any + Send>>,
191 }
192
193 #[inline]
194 fn do_call<F: FnOnce() -> R, R>(data: *mut u8) {
195 // SAFETY: this is the responsibility of the caller, see above.
196 unsafe {
197 let data = data.cast::<Data<F, R>>();
198 let data = &mut (*data);
199 let f = ManuallyDrop::take(&mut data.f);
200 data.r = ManuallyDrop::new(f());
201 }
202 }
203
204 #[cold]
205 #[rustc_nounwind] // `intrinsic::catch_unwind` requires catch fn to be nounwind
206 fn do_catch<F: FnOnce() -> R, R>(data: *mut u8, exception: *mut u8) {
207 let data = data.cast::<Data<F, R>>();
208 // Safety: data is correctly initialized
209 let data = unsafe { &mut (*data) };
210
211 // Safety: exception comes from the Rust intrinsic, not much we do other than trust it
212 match unsafe { Exception::unwrap(exception.cast()) } {
213 Ok(p) => data.p = ManuallyDrop::new(p),
214 Err(err) => {
215 tracing::error!("Failed to catch exception: {err:?}");
216 abort();
217 }
218 }
219 }
220
221 let mut data = Data {
222 f: ManuallyDrop::new(f),
223 };
224 let data_ptr = addr_of_mut!(data).cast::<u8>();
225
226 // Safety: intrinsic call
227 unsafe {
228 if intrinsics::catch_unwind(do_call::<F, R>, data_ptr, do_catch::<F, R>) == 0 {
229 Ok(ManuallyDrop::into_inner(data.r))
230 } else {
231 Err(ManuallyDrop::into_inner(data.p))
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use alloc::boxed::Box;
240
241 #[test]
242 fn begin_and_catch_roundtrip() {
243 let res = catch_unwind(|| {
244 begin_unwind(Box::new(42)).unwrap();
245 })
246 .map_err(|err| *err.downcast_ref::<i32>().unwrap());
247 assert_eq!(res, Err(42));
248 }
249}