Skip to content

Commit 8a87b94

Browse files
committed
Auto merge of #67711 - Amanieu:fix_unwind_leak, r=alexcrichton
Fix memory leak if C++ catches a Rust panic and discards it If C++ catches a Rust panic using `catch (...)` and then chooses not to rethrow it, the `Box<dyn Any>` in the exception may be leaked. This PR fixes this by adding the necessary destructors to the exception object. r? @Mark-Simulacrum
2 parents cb6122d + 25519e5 commit 8a87b94

File tree

7 files changed

+139
-26
lines changed

7 files changed

+139
-26
lines changed

src/libpanic_unwind/emcc.rs

+34-7
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,49 @@ pub fn payload() -> *mut u8 {
5252
ptr::null_mut()
5353
}
5454

55+
struct Exception {
56+
// This needs to be an Option because the object's lifetime follows C++
57+
// semantics: when catch_unwind moves the Box out of the exception it must
58+
// still leave the exception object in a valid state because its destructor
59+
// is still going to be called by __cxa_end_catch..
60+
data: Option<Box<dyn Any + Send>>,
61+
}
62+
5563
pub unsafe fn cleanup(ptr: *mut u8) -> Box<dyn Any + Send> {
5664
assert!(!ptr.is_null());
57-
let adjusted_ptr = __cxa_begin_catch(ptr as *mut libc::c_void);
58-
let ex = ptr::read(adjusted_ptr as *mut _);
65+
let adjusted_ptr = __cxa_begin_catch(ptr as *mut libc::c_void) as *mut Exception;
66+
let ex = (*adjusted_ptr).data.take();
5967
__cxa_end_catch();
60-
ex
68+
ex.unwrap()
6169
}
6270

6371
pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
6472
let sz = mem::size_of_val(&data);
65-
let exception = __cxa_allocate_exception(sz);
73+
let exception = __cxa_allocate_exception(sz) as *mut Exception;
6674
if exception.is_null() {
6775
return uw::_URC_FATAL_PHASE1_ERROR as u32;
6876
}
69-
ptr::write(exception as *mut _, data);
70-
__cxa_throw(exception as *mut _, &EXCEPTION_TYPE_INFO, ptr::null_mut());
77+
ptr::write(exception, Exception { data: Some(data) });
78+
__cxa_throw(exception as *mut _, &EXCEPTION_TYPE_INFO, exception_cleanup);
79+
}
80+
81+
// On WASM and ARM, the destructor returns the pointer to the object.
82+
cfg_if::cfg_if! {
83+
if #[cfg(any(target_arch = "arm", target_arch = "wasm32"))] {
84+
type DestructorRet = *mut libc::c_void;
85+
} else {
86+
type DestructorRet = ();
87+
}
88+
}
89+
extern "C" fn exception_cleanup(ptr: *mut libc::c_void) -> DestructorRet {
90+
unsafe {
91+
if let Some(b) = (ptr as *mut Exception).read().data {
92+
drop(b);
93+
super::__rust_drop_panic();
94+
}
95+
#[cfg(any(target_arch = "arm", target_arch = "wasm32"))]
96+
ptr
97+
}
7198
}
7299

73100
#[lang = "eh_personality"]
@@ -89,7 +116,7 @@ extern "C" {
89116
fn __cxa_throw(
90117
thrown_exception: *mut libc::c_void,
91118
tinfo: *const TypeInfo,
92-
dest: *mut libc::c_void,
119+
dest: extern "C" fn(*mut libc::c_void) -> DestructorRet,
93120
) -> !;
94121
fn __gxx_personality_v0(
95122
version: c_int,

src/libpanic_unwind/gcc.rs

+5-6
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ use unwind as uw;
5757
#[repr(C)]
5858
struct Exception {
5959
_uwe: uw::_Unwind_Exception,
60-
cause: Option<Box<dyn Any + Send>>,
60+
cause: Box<dyn Any + Send>,
6161
}
6262

6363
pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
@@ -67,7 +67,7 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
6767
exception_cleanup,
6868
private: [0; uw::unwinder_private_data_size],
6969
},
70-
cause: Some(data),
70+
cause: data,
7171
});
7272
let exception_param = Box::into_raw(exception) as *mut uw::_Unwind_Exception;
7373
return uw::_Unwind_RaiseException(exception_param) as u32;
@@ -78,6 +78,7 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
7878
) {
7979
unsafe {
8080
let _: Box<Exception> = Box::from_raw(exception as *mut Exception);
81+
super::__rust_drop_panic();
8182
}
8283
}
8384
}
@@ -87,10 +88,8 @@ pub fn payload() -> *mut u8 {
8788
}
8889

8990
pub unsafe fn cleanup(ptr: *mut u8) -> Box<dyn Any + Send> {
90-
let my_ep = ptr as *mut Exception;
91-
let cause = (*my_ep).cause.take();
92-
uw::_Unwind_DeleteException(ptr as *mut _);
93-
cause.unwrap()
91+
let exception = Box::from_raw(ptr as *mut Exception);
92+
exception.cause
9493
}
9594

9695
// Rust's exception class identifier. This is used by personality routines to

src/libpanic_unwind/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#![feature(staged_api)]
2727
#![feature(std_internals)]
2828
#![feature(unwind_attributes)]
29+
#![feature(abi_thiscall)]
2930
#![panic_runtime]
3031
#![feature(panic_runtime)]
3132

@@ -60,6 +61,12 @@ cfg_if::cfg_if! {
6061
}
6162
}
6263

64+
extern "C" {
65+
/// Handler in libstd called when a panic object is dropped outside of
66+
/// `catch_unwind`.
67+
fn __rust_drop_panic() -> !;
68+
}
69+
6370
mod dwarf;
6471

6572
// Entry point for catching an exception, implemented using the `try` intrinsic

src/libpanic_unwind/seh.rs

+58-7
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,11 @@ use libc::{c_int, c_uint, c_void};
7777
// #include <stdint.h>
7878
//
7979
// struct rust_panic {
80+
// rust_panic(const rust_panic&);
81+
// ~rust_panic();
82+
//
8083
// uint64_t x[2];
81-
// }
84+
// };
8285
//
8386
// void foo() {
8487
// rust_panic a = {0, 1};
@@ -128,7 +131,7 @@ mod imp {
128131
#[repr(C)]
129132
pub struct _ThrowInfo {
130133
pub attributes: c_uint,
131-
pub pnfnUnwind: imp::ptr_t,
134+
pub pmfnUnwind: imp::ptr_t,
132135
pub pForwardCompat: imp::ptr_t,
133136
pub pCatchableTypeArray: imp::ptr_t,
134137
}
@@ -145,7 +148,7 @@ pub struct _CatchableType {
145148
pub pType: imp::ptr_t,
146149
pub thisDisplacement: _PMD,
147150
pub sizeOrOffset: c_int,
148-
pub copy_function: imp::ptr_t,
151+
pub copyFunction: imp::ptr_t,
149152
}
150153

151154
#[repr(C)]
@@ -168,7 +171,7 @@ const TYPE_NAME: [u8; 11] = *b"rust_panic\0";
168171

169172
static mut THROW_INFO: _ThrowInfo = _ThrowInfo {
170173
attributes: 0,
171-
pnfnUnwind: ptr!(0),
174+
pmfnUnwind: ptr!(0),
172175
pForwardCompat: ptr!(0),
173176
pCatchableTypeArray: ptr!(0),
174177
};
@@ -181,7 +184,7 @@ static mut CATCHABLE_TYPE: _CatchableType = _CatchableType {
181184
pType: ptr!(0),
182185
thisDisplacement: _PMD { mdisp: 0, pdisp: -1, vdisp: 0 },
183186
sizeOrOffset: mem::size_of::<[u64; 2]>() as c_int,
184-
copy_function: ptr!(0),
187+
copyFunction: ptr!(0),
185188
};
186189

187190
extern "C" {
@@ -208,6 +211,43 @@ static mut TYPE_DESCRIPTOR: _TypeDescriptor = _TypeDescriptor {
208211
name: TYPE_NAME,
209212
};
210213

214+
// Destructor used if the C++ code decides to capture the exception and drop it
215+
// without propagating it. The catch part of the try intrinsic will set the
216+
// first word of the exception object to 0 so that it is skipped by the
217+
// destructor.
218+
//
219+
// Note that x86 Windows uses the "thiscall" calling convention for C++ member
220+
// functions instead of the default "C" calling convention.
221+
//
222+
// The exception_copy function is a bit special here: it is invoked by the MSVC
223+
// runtime under a try/catch block and the panic that we generate here will be
224+
// used as the result of the exception copy. This is used by the C++ runtime to
225+
// support capturing exceptions with std::exception_ptr, which we can't support
226+
// because Box<dyn Any> isn't clonable.
227+
macro_rules! define_cleanup {
228+
($abi:tt) => {
229+
unsafe extern $abi fn exception_cleanup(e: *mut [u64; 2]) {
230+
if (*e)[0] != 0 {
231+
cleanup(*e);
232+
super::__rust_drop_panic();
233+
}
234+
}
235+
#[unwind(allowed)]
236+
unsafe extern $abi fn exception_copy(_dest: *mut [u64; 2],
237+
_src: *mut [u64; 2])
238+
-> *mut [u64; 2] {
239+
panic!("Rust panics cannot be copied");
240+
}
241+
}
242+
}
243+
cfg_if::cfg_if! {
244+
if #[cfg(target_arch = "x86")] {
245+
define_cleanup!("thiscall");
246+
} else {
247+
define_cleanup!("C");
248+
}
249+
}
250+
211251
pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
212252
use core::intrinsics::atomic_store;
213253

@@ -220,8 +260,7 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
220260
// exception (constructed above).
221261
let ptrs = mem::transmute::<_, raw::TraitObject>(data);
222262
let mut ptrs = [ptrs.data as u64, ptrs.vtable as u64];
223-
let ptrs_ptr = ptrs.as_mut_ptr();
224-
let throw_ptr = ptrs_ptr as *mut _;
263+
let throw_ptr = ptrs.as_mut_ptr() as *mut _;
225264

226265
// This... may seems surprising, and justifiably so. On 32-bit MSVC the
227266
// pointers between these structure are just that, pointers. On 64-bit MSVC,
@@ -243,6 +282,12 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
243282
//
244283
// In any case, we basically need to do something like this until we can
245284
// express more operations in statics (and we may never be able to).
285+
if !cfg!(bootstrap) {
286+
atomic_store(
287+
&mut THROW_INFO.pmfnUnwind as *mut _ as *mut u32,
288+
ptr!(exception_cleanup) as u32,
289+
);
290+
}
246291
atomic_store(
247292
&mut THROW_INFO.pCatchableTypeArray as *mut _ as *mut u32,
248293
ptr!(&CATCHABLE_TYPE_ARRAY as *const _) as u32,
@@ -255,6 +300,12 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
255300
&mut CATCHABLE_TYPE.pType as *mut _ as *mut u32,
256301
ptr!(&TYPE_DESCRIPTOR as *const _) as u32,
257302
);
303+
if !cfg!(bootstrap) {
304+
atomic_store(
305+
&mut CATCHABLE_TYPE.copyFunction as *mut _ as *mut u32,
306+
ptr!(exception_copy) as u32,
307+
);
308+
}
258309

259310
extern "system" {
260311
#[unwind(allowed)]

src/librustc_codegen_llvm/intrinsic.rs

+26-5
Original file line numberDiff line numberDiff line change
@@ -922,40 +922,61 @@ fn codegen_msvc_try(
922922
// #include <stdint.h>
923923
//
924924
// struct rust_panic {
925+
// rust_panic(const rust_panic&);
926+
// ~rust_panic();
927+
//
925928
// uint64_t x[2];
926929
// }
927930
//
928931
// int bar(void (*foo)(void), uint64_t *ret) {
929932
// try {
930933
// foo();
931934
// return 0;
932-
// } catch(rust_panic a) {
935+
// } catch(rust_panic& a) {
933936
// ret[0] = a.x[0];
934937
// ret[1] = a.x[1];
938+
// a.x[0] = 0;
935939
// return 1;
936940
// }
937941
// }
938942
//
939943
// More information can be found in libstd's seh.rs implementation.
940944
let i64_2 = bx.type_array(bx.type_i64(), 2);
941-
let i64_align = bx.tcx().data_layout.i64_align.abi;
942-
let slot = bx.alloca(i64_2, i64_align);
945+
let i64_2_ptr = bx.type_ptr_to(i64_2);
946+
let ptr_align = bx.tcx().data_layout.pointer_align.abi;
947+
let slot = bx.alloca(i64_2_ptr, ptr_align);
943948
bx.invoke(func, &[data], normal.llbb(), catchswitch.llbb(), None);
944949

945950
normal.ret(bx.const_i32(0));
946951

947952
let cs = catchswitch.catch_switch(None, None, 1);
948953
catchswitch.add_handler(cs, catchpad.llbb());
949954

955+
// The flag value of 8 indicates that we are catching the exception by
956+
// reference instead of by value. We can't use catch by value because
957+
// that requires copying the exception object, which we don't support
958+
// since our exception object effectively contains a Box.
959+
//
960+
// Source: MicrosoftCXXABI::getAddrOfCXXCatchHandlerType in clang
961+
let flags = bx.const_i32(8);
950962
let tydesc = match bx.tcx().lang_items().eh_catch_typeinfo() {
951963
Some(did) => bx.get_static(did),
952964
None => bug!("eh_catch_typeinfo not defined, but needed for SEH unwinding"),
953965
};
954-
let funclet = catchpad.catch_pad(cs, &[tydesc, bx.const_i32(0), slot]);
966+
let funclet = catchpad.catch_pad(cs, &[tydesc, flags, slot]);
955967

956-
let payload = catchpad.load(slot, i64_align);
968+
let i64_align = bx.tcx().data_layout.i64_align.abi;
969+
let payload_ptr = catchpad.load(slot, ptr_align);
970+
let payload = catchpad.load(payload_ptr, i64_align);
957971
let local_ptr = catchpad.bitcast(local_ptr, bx.type_ptr_to(i64_2));
958972
catchpad.store(payload, local_ptr, i64_align);
973+
974+
// Clear the first word of the exception so avoid double-dropping it.
975+
// This will be read by the destructor which is implicitly called at the
976+
// end of the catch block by the runtime.
977+
let payload_0_ptr = catchpad.inbounds_gep(payload_ptr, &[bx.const_i32(0), bx.const_i32(0)]);
978+
catchpad.store(bx.const_u64(0), payload_0_ptr, i64_align);
979+
959980
catchpad.catch_ret(&funclet, caught.llbb());
960981

961982
caught.ret(bx.const_i32(1));

src/libstd/panicking.rs

+9
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ extern "C" {
5555
fn __rust_start_panic(payload: usize) -> u32;
5656
}
5757

58+
/// This function is called by the panic runtime if FFI code catches a Rust
59+
/// panic but doesn't rethrow it. We don't support this case since it messes
60+
/// with our panic count.
61+
#[cfg(not(test))]
62+
#[rustc_std_internal_symbol]
63+
extern "C" fn __rust_drop_panic() -> ! {
64+
rtabort!("Rust panics must be rethrown");
65+
}
66+
5867
#[derive(Copy, Clone)]
5968
enum Hook {
6069
Default,

src/test/run-make-fulldeps/foreign-exceptions/foo.rs

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
// For linking libstdc++ on MinGW
66
#![cfg_attr(all(windows, target_env = "gnu"), feature(static_nobundle))]
7-
87
#![feature(unwind_attributes)]
98

109
use std::panic::{catch_unwind, AssertUnwindSafe};

0 commit comments

Comments
 (0)