Skip to content

Commit

Permalink
Fix panics in intercept under SEH
Browse files Browse the repository at this point in the history
  • Loading branch information
purplesyringa committed Oct 31, 2024
1 parent deca2c2 commit b7ba7c1
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 17 deletions.
5 changes: 3 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ pub unsafe fn intercept<R, E>(func: impl FnOnce() -> R) -> Result<R, (E, InFligh
#[cfg(test)]
mod test {
use super::*;
use std::panic::AssertUnwindSafe;

#[test]
fn catch_ok() {
Expand All @@ -200,6 +199,7 @@ mod test {
assert_eq!(result.unwrap_err(), "Hello, world!");
}

#[cfg(feature = "std")]
#[test]
fn catch_panic() {
struct Dropper<'a>(&'a mut bool);
Expand All @@ -210,7 +210,7 @@ mod test {
}

let mut destructor_was_run = false;
std::panic::catch_unwind(AssertUnwindSafe(|| {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _dropper = Dropper(&mut destructor_was_run);
let _: Result<(), ()> = unsafe { catch(|| panic!("Hello, world!")) };
}))
Expand All @@ -233,6 +233,7 @@ mod test {
assert_eq!(result.unwrap_err(), "Hello, world! You look nice btw.");
}

#[cfg(feature = "std")]
#[test]
fn panic_while_in_flight() {
struct Dropper;
Expand Down
1 change: 1 addition & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ mod test {
assert_eq!(caught_ex, "Hello, world!");
}

#[cfg(feature = "std")]
#[test]
fn intercept_panic() {
let result = std::panic::catch_unwind(|| unsafe {
Expand Down
128 changes: 114 additions & 14 deletions src/backend/seh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use core::marker::{FnPtr, PhantomData};
use core::mem::ManuallyDrop;
use core::sync::atomic::{AtomicU32, Ordering};

#[cfg(feature = "std")]
use core::panic::PanicPayload;

pub(crate) struct ActiveBackend;

/// SEH-based unwinding.
Expand Down Expand Up @@ -51,33 +54,66 @@ unsafe impl ThrowByValue for ActiveBackend {

#[inline]
unsafe fn intercept<Func: FnOnce() -> R, R, E>(func: Func) -> Result<R, (E, SehRethrowHandle)> {
enum CaughtUnwind<E> {
LithiumException(E),

#[cfg(feature = "std")]
RustPanic(Box<dyn core::any::Any + Send + 'static>),
}

let catch = |ex: *mut u8| {
let ex: *mut Exception<E> = ex.cast();
// This callback is not allowed to unwind, so we can't rethrow exceptions.
if ex.is_null() {
// This is a foreign exception. Abort from a separate function to prevent inlining.
abort_on_foreign_exception();
}

let ex_lithium: *mut Exception<E> = ex.cast();

// Rethrow foreign exceptions, as well as Rust panics.
// SAFETY: If `ex` is non-null, it's a `rust_panic` exception, which can either be
// thrown by us or by the Rust runtime; both have the `header.canary` field as the first
// field in their structures.
if ex.is_null() || unsafe { (*ex).header.canary } != (&raw const THROW_INFO).cast() {
// SAFETY: Rethrowing is always valid.
unsafe {
cxx_throw(core::ptr::null_mut(), core::ptr::null());
if unsafe { (*ex_lithium).header.canary } != (&raw const THROW_INFO).cast() {
// This is a Rust exception
#[cfg(feature = "std")]
{
// We can't rethrow it immediately from this nounwind callback, so let's catch
// it first.
// SAFETY: `ex` is the callback value of `core::intrinsics::catch_unwind`.
let payload = unsafe { __rust_panic_cleanup(ex) };
// SAFETY: `__rust_panic_cleanup` returns a Box.
let payload = unsafe { Box::from_raw(payload) };
return CaughtUnwind::RustPanic(payload);
}
#[cfg(not(feature = "std"))]
{
// In no-std mode, we can't handle this.
core::intrinsics::abort();
}
}

// We catch the exception by reference, so the C++ runtime will drop it. Tell our
// destructor to calm down.
// SAFETY: This is our exception, so `ex` points at a valid instance of `Exception<E>`.
// SAFETY: This is our exception, so `ex_lithium` points at a valid instance of
// `Exception<E>`.
unsafe {
(*ex).header.caught = true;
(*ex_lithium).header.caught = true;
}
// SAFETY: As above.
let cause = unsafe { &mut (*ex).cause };
let cause = unsafe { &mut (*ex_lithium).cause };
// SAFETY: We only read the cause here, so no double copies.
(unsafe { ManuallyDrop::take(cause) }, SehRethrowHandle)
CaughtUnwind::LithiumException(unsafe { ManuallyDrop::take(cause) })
};

unsafe { intercept(func, catch) }
// SAFETY: `catch` doesn't unwind.
match unsafe { intercept(func, catch) } {
Ok(value) => Ok(value),

Err(CaughtUnwind::LithiumException(cause)) => Err((cause, SehRethrowHandle)),

#[cfg(feature = "std")]
Err(CaughtUnwind::RustPanic(payload)) => throw_std_panic(payload),
}
}
}

Expand Down Expand Up @@ -227,7 +263,7 @@ static THROW_INFO: ThrowInfo = ThrowInfo {
catchable_type_array: SmallPtr::null(), // filled by throw
};

fn abort() -> ! {
fn abort_on_caught_by_cxx() -> ! {
#[cfg(feature = "std")]
{
eprintln!(
Expand All @@ -239,18 +275,32 @@ fn abort() -> ! {
core::intrinsics::abort();
}

#[cold]
#[inline(never)]
fn abort_on_foreign_exception() -> ! {
#[cfg(feature = "std")]
{
eprintln!(
"Lithium caught a foreign exception. This is unsupported. The process will now terminate.",
);
std::process::abort();
}
#[cfg(not(feature = "std"))]
core::intrinsics::abort();
}

macro_rules! define_fns {
($abi:tt) => {
unsafe extern $abi fn cleanup(ex: *mut ExceptionHeader) {
// SAFETY: `ex` is a `this` pointer when called by the C++ runtime.
if !unsafe { (*ex).caught } {
// Caught by the cxx runtime
abort();
abort_on_caught_by_cxx();
}
}

unsafe extern $abi fn copy(_to: *mut ExceptionHeader, _from: *const ExceptionHeader) -> *mut ExceptionHeader {
abort();
abort_on_caught_by_cxx();
}
};
}
Expand Down Expand Up @@ -327,6 +377,56 @@ extern "system-unwind" {
) -> !;
}

#[cfg(feature = "std")]
extern "Rust" {
fn __rust_start_panic(payload: &mut dyn PanicPayload) -> u32;
}

#[cfg(feature = "std")]
extern "C" {
#[expect(improper_ctypes, reason = "Copied from std")]
fn __rust_panic_cleanup(payload: *mut u8) -> *mut (dyn core::any::Any + Send + 'static);
}

#[cfg(feature = "std")]
fn throw_std_panic(payload: Box<dyn core::any::Any + Send + 'static>) -> ! {
// We can't use resume_unwind here, as it increments the panic count, and we didn't decrement it
// upon catching the panic. Call `__rust_start_panic` directly instead.
use core::any::Any;

struct RewrapBox(Box<dyn Any + Send + 'static>);

// SAFETY: Copied straight from std.
unsafe impl PanicPayload for RewrapBox {
fn take_box(&mut self) -> *mut (dyn Any + Send) {
Box::into_raw(core::mem::replace(&mut self.0, Box::new(())))
}
fn get(&mut self) -> &(dyn Any + Send) {
&*self.0
}
}

impl core::fmt::Display for RewrapBox {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let payload = &self.0;
let payload = if let Some(&s) = payload.downcast_ref::<&'static str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"Box<dyn Any>"
};
f.write_str(payload)
}
}

// SAFETY: Copied straight from std.
unsafe {
__rust_start_panic(&mut RewrapBox(payload));
}
core::intrinsics::abort();
}

/// Throw a C++ exception.
///
/// # Safety
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
)
)]
#![cfg_attr(backend = "itanium", feature(core_intrinsics))]
#![cfg_attr(backend = "seh", feature(core_intrinsics, fn_ptr_trait))]
#![cfg_attr(backend = "seh", feature(core_intrinsics, fn_ptr_trait, std_internals))]
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(
clippy::cargo,
Expand Down

0 comments on commit b7ba7c1

Please sign in to comment.