Skip to content

Commit b3cf73a

Browse files
committed
Validate exception types in debug
1 parent 0581d4a commit b3cf73a

File tree

4 files changed

+124
-12
lines changed

4 files changed

+124
-12
lines changed

src/backend/seh.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::{
66
super::{abort, intrinsic::intercept},
77
RethrowHandle, ThrowByValue,
88
};
9+
use crate::type_checker::TypeChecker;
910
use alloc::boxed::Box;
1011
use core::any::Any;
1112
use core::marker::{FnPtr, PhantomData};
@@ -89,12 +90,16 @@ unsafe impl ThrowByValue for ActiveBackend {
8990

9091
// We catch the exception by reference, so the C++ runtime will drop it. Tell our
9192
// destructor to calm down.
93+
94+
// SAFETY: This is our exception, so `ex_lithium` at least points at a valid instance of
95+
// `ExceptionHeader`. We don't really want to assert that it's `Exception<E>` yet, since
96+
// the E might be wrong and we want to guard against that in debug.
97+
let header = unsafe { &mut (*ex_lithium).header };
98+
header.caught = true;
99+
header.type_checker.expect::<E>();
100+
92101
// SAFETY: This is our exception, so `ex_lithium` points at a valid instance of
93102
// `Exception<E>`.
94-
unsafe {
95-
(*ex_lithium).header.caught = true;
96-
}
97-
// SAFETY: As above.
98103
let cause = unsafe { &mut (*ex_lithium).cause };
99104
// SAFETY: We only read the cause here, so no double copies.
100105
CaughtUnwind::LithiumException(unsafe { ManuallyDrop::take(cause) })
@@ -131,6 +136,7 @@ unsafe fn do_throw<E>(cause: E) -> ! {
131136
header: ExceptionHeader {
132137
canary: (&raw const THROW_INFO).cast(), // any static will work
133138
caught: false,
139+
type_checker: TypeChecker::new::<E>(),
134140
},
135141
cause: ManuallyDrop::new(cause),
136142
};
@@ -145,6 +151,7 @@ unsafe fn do_throw<E>(cause: E) -> ! {
145151
struct ExceptionHeader {
146152
canary: *const (), // From Rust ABI
147153
caught: bool,
154+
type_checker: TypeChecker,
148155
}
149156

150157
#[repr(C)]
@@ -354,11 +361,15 @@ impl<T: ?Sized> SmallPtr<*const T> {
354361
}
355362

356363
unsafe extern "system-unwind" {
364+
#[allow(
365+
improper_ctypes,
366+
reason = "false positive that TypeId is not FFI-safe in debug"
367+
)]
357368
fn RaiseException(
358369
code: u32,
359370
flags: u32,
360371
n_parameters: u32,
361-
paremeters: *mut ExceptionRecordParameters,
372+
parameters: *mut ExceptionRecordParameters,
362373
) -> !;
363374
}
364375

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ mod stacked_exceptions;
287287
))]
288288
mod intrinsic;
289289

290+
mod type_checker;
291+
290292
pub use api::{InFlightException, catch, intercept, throw};
291293

292294
/// Abort the process with a message.

src/stacked_exceptions.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::{
22
backend::{ActiveBackend, RethrowHandle, ThrowByPointer, ThrowByValue},
33
heterogeneous_stack::unbounded::Stack,
44
};
5+
use crate::type_checker::TypeChecker;
56
use core::mem::{ManuallyDrop, offset_of};
67

78
// Module invariant: thrown exceptions of type `E` are passed to the pointer-throwing backend as
@@ -82,7 +83,12 @@ impl<E> RethrowHandle for PointerRethrowHandle<E> {
8283
}
8384
}
8485

85-
type Header = <ActiveBackend as ThrowByPointer>::ExceptionHeader;
86+
type BackendHeader = <ActiveBackend as ThrowByPointer>::ExceptionHeader;
87+
88+
struct Header {
89+
backend_header: ManuallyDrop<BackendHeader>,
90+
type_checker: TypeChecker,
91+
}
8692

8793
/// An exception object, to be used by the backend.
8894
///
@@ -92,7 +98,7 @@ type Header = <ActiveBackend as ThrowByPointer>::ExceptionHeader;
9298
/// accidentally caught by user code.
9399
#[repr(C)] // ensure `header` is at the same offset regardless of `E`
94100
pub struct Exception<E> {
95-
header: ManuallyDrop<Header>,
101+
header: Header,
96102
cause: ManuallyDrop<Unaligned<E>>,
97103
}
98104

@@ -103,7 +109,10 @@ impl<E> Exception<E> {
103109
/// Create a new exception to be thrown.
104110
fn new(cause: E) -> Self {
105111
Self {
106-
header: ManuallyDrop::new(ActiveBackend::new_header()),
112+
header: Header {
113+
backend_header: ManuallyDrop::new(ActiveBackend::new_header()),
114+
type_checker: TypeChecker::new::<E>(),
115+
},
107116
cause: ManuallyDrop::new(Unaligned(cause)),
108117
}
109118
}
@@ -113,9 +122,9 @@ impl<E> Exception<E> {
113122
/// # Safety
114123
///
115124
/// `ex` must be a unique pointer at an exception object.
116-
pub const unsafe fn header(ex: *mut Self) -> *mut Header {
125+
pub const unsafe fn header(ex: *mut Self) -> *mut BackendHeader {
117126
// SAFETY: Required transitively.
118-
unsafe { &raw mut (*ex).header }.cast()
127+
unsafe { &raw mut (*ex).header.backend_header }.cast()
119128
}
120129

121130
/// Restore pointer from pointer to header.
@@ -124,9 +133,9 @@ impl<E> Exception<E> {
124133
///
125134
/// `header` must have been produced by [`Exception::header`], and the corresponding object must
126135
/// be alive.
127-
pub const unsafe fn from_header(header: *mut Header) -> *mut Self {
136+
pub const unsafe fn from_header(header: *mut BackendHeader) -> *mut Self {
128137
// SAFETY: Required transitively.
129-
unsafe { header.byte_sub(offset_of!(Self, header)) }.cast()
138+
unsafe { header.byte_sub(offset_of!(Self, header.backend_header)) }.cast()
130139
}
131140

132141
/// Get the cause of the exception.
@@ -136,6 +145,7 @@ impl<E> Exception<E> {
136145
/// This function returns a bitwise copy of the cause. This means that it can only be called
137146
/// once on each exception.
138147
pub unsafe fn cause(&mut self) -> E {
148+
self.header.type_checker.expect::<E>();
139149
// SAFETY: We transitively require that the cause is not read twice.
140150
unsafe { ManuallyDrop::take(&mut self.cause).0 }
141151
}
@@ -227,6 +237,10 @@ pub unsafe fn replace_last<E, F>(ex: *mut Exception<E>, cause: F) -> *mut Except
227237
if const { get_alloc_size::<E>() == get_alloc_size::<F>() } {
228238
// Reuse existing allocation without populating the header again
229239
let ex: *mut Exception<F> = ex.cast();
240+
// SAFETY: Type checker is stored at the same offset regardless of type.
241+
unsafe {
242+
(*ex).header.type_checker = TypeChecker::new::<F>();
243+
}
230244
// SAFETY: If `ex.cause` was valid for writes for `size_of::<E>()` bytes, it should be valid
231245
// for `size_of::<F>()` bytes as well because those are equal.
232246
unsafe {

src/type_checker.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#[cfg(debug_assertions)]
2+
mod imp {
3+
use crate::abort;
4+
use core::any::{TypeId, type_name};
5+
use core::marker::PhantomData;
6+
7+
/// A best-effort runtime type checker for thrown and caught exception.
8+
///
9+
/// Only enabled in debug, zero-cost in release.
10+
pub struct TypeChecker {
11+
id: TypeId,
12+
name: &'static str,
13+
}
14+
15+
impl TypeChecker {
16+
// Create RTTI for `T`.
17+
pub fn new<T: ?Sized>() -> Self {
18+
Self {
19+
id: type_id::<T>(),
20+
name: type_name::<T>(),
21+
}
22+
}
23+
24+
// Validate that `T` matches the stored RTTI (best-effort only).
25+
pub fn expect<T: ?Sized>(&self) {
26+
if self.id != type_id::<T>() {
27+
abort(&alloc::format!(
28+
"lithium::catch::<_, {}> caught an exception of type {}. This is undefined behavior. The process will now terminate.\n",
29+
core::any::type_name::<T>(),
30+
self.name,
31+
));
32+
}
33+
}
34+
}
35+
36+
fn type_id<T: ?Sized>() -> TypeId {
37+
// This implements `core::any::type_id` for non-`'static` types and is simply copied from
38+
// dtolnay's `typeid` crate. That's a small crate, but keeping Lithium zero-dep sounds
39+
// worthwhile, and it'll probably slightly improve compilation times.
40+
trait NonStaticAny {
41+
fn get_type_id(&self) -> TypeId
42+
where
43+
Self: 'static;
44+
}
45+
46+
impl<T: ?Sized> NonStaticAny for PhantomData<T> {
47+
fn get_type_id(&self) -> TypeId
48+
where
49+
Self: 'static,
50+
{
51+
TypeId::of::<T>()
52+
}
53+
}
54+
55+
NonStaticAny::get_type_id(
56+
// SAFETY: Just a lifetime transmute, we never handle references with the extended
57+
// lifetime.
58+
unsafe {
59+
core::mem::transmute::<&dyn NonStaticAny, &(dyn NonStaticAny + 'static)>(
60+
&PhantomData::<T>,
61+
)
62+
},
63+
)
64+
}
65+
}
66+
67+
#[cfg(not(debug_assertions))]
68+
mod imp {
69+
/// A best-effort runtime type checker for thrown and caught exception.
70+
///
71+
/// Only enabled in debug, zero-cost in release.
72+
pub struct TypeChecker;
73+
74+
impl TypeChecker {
75+
// Create RTTI for `T`.
76+
pub fn new<T: ?Sized>() -> Self {
77+
Self
78+
}
79+
80+
// Validate that `T` matches the stored RTTI (best-effort only).
81+
pub fn expect<T: ?Sized>(&self) {}
82+
}
83+
}
84+
85+
pub use imp::TypeChecker;

0 commit comments

Comments
 (0)