Skip to content

Fix memory leak if C++ catches a Rust panic and discards it #67711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 14, 2020
41 changes: 34 additions & 7 deletions src/libpanic_unwind/emcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,49 @@ pub fn payload() -> *mut u8 {
ptr::null_mut()
}

struct Exception {
// This needs to be an Option because the object's lifetime follows C++
// semantics: when catch_unwind moves the Box out of the exception it must
// still leave the exception object in a valid state because its destructor
// is still going to be called by __cxa_end_catch..
data: Option<Box<dyn Any + Send>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading over this it seemed odd that the Option went away below but seemed to be added here, but this is for emcc.rs so I'm not reviewing it too too closely.

Copy link
Member Author

@Amanieu Amanieu Jan 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an option here because the C++ runtime will invoke the destructor during __cxa_end_catch. We need to ensure that the exception object contains None at that point to avoid a double-free.

}

pub unsafe fn cleanup(ptr: *mut u8) -> Box<dyn Any + Send> {
assert!(!ptr.is_null());
let adjusted_ptr = __cxa_begin_catch(ptr as *mut libc::c_void);
let ex = ptr::read(adjusted_ptr as *mut _);
let adjusted_ptr = __cxa_begin_catch(ptr as *mut libc::c_void) as *mut Exception;
let ex = (*adjusted_ptr).data.take();
__cxa_end_catch();
ex
ex.unwrap()
}

pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
let sz = mem::size_of_val(&data);
let exception = __cxa_allocate_exception(sz);
let exception = __cxa_allocate_exception(sz) as *mut Exception;
if exception.is_null() {
return uw::_URC_FATAL_PHASE1_ERROR as u32;
}
ptr::write(exception as *mut _, data);
__cxa_throw(exception as *mut _, &EXCEPTION_TYPE_INFO, ptr::null_mut());
ptr::write(exception, Exception { data: Some(data) });
__cxa_throw(exception as *mut _, &EXCEPTION_TYPE_INFO, exception_cleanup);
}

// On WASM and ARM, the destructor returns the pointer to the object.
cfg_if::cfg_if! {
if #[cfg(any(target_arch = "arm", target_arch = "wasm32"))] {
type DestructorRet = *mut libc::c_void;
} else {
type DestructorRet = ();
}
}
extern "C" fn exception_cleanup(ptr: *mut libc::c_void) -> DestructorRet {
unsafe {
if let Some(b) = (ptr as *mut Exception).read().data {
drop(b);
super::__rust_drop_panic();
}
#[cfg(any(target_arch = "arm", target_arch = "wasm32"))]
ptr
}
}

#[lang = "eh_personality"]
Expand All @@ -89,7 +116,7 @@ extern "C" {
fn __cxa_throw(
thrown_exception: *mut libc::c_void,
tinfo: *const TypeInfo,
dest: *mut libc::c_void,
dest: extern "C" fn(*mut libc::c_void) -> DestructorRet,
) -> !;
fn __gxx_personality_v0(
version: c_int,
Expand Down
11 changes: 5 additions & 6 deletions src/libpanic_unwind/gcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use unwind as uw;
#[repr(C)]
struct Exception {
_uwe: uw::_Unwind_Exception,
cause: Option<Box<dyn Any + Send>>,
cause: Box<dyn Any + Send>,
}

pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
Expand All @@ -67,7 +67,7 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
exception_cleanup,
private: [0; uw::unwinder_private_data_size],
},
cause: Some(data),
cause: data,
});
let exception_param = Box::into_raw(exception) as *mut uw::_Unwind_Exception;
return uw::_Unwind_RaiseException(exception_param) as u32;
Expand All @@ -78,6 +78,7 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
) {
unsafe {
let _: Box<Exception> = Box::from_raw(exception as *mut Exception);
super::__rust_drop_panic();
}
}
}
Expand All @@ -87,10 +88,8 @@ pub fn payload() -> *mut u8 {
}

pub unsafe fn cleanup(ptr: *mut u8) -> Box<dyn Any + Send> {
let my_ep = ptr as *mut Exception;
let cause = (*my_ep).cause.take();
uw::_Unwind_DeleteException(ptr as *mut _);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who calls DeleteException on the exception? There's not a drop impl on Exception or _Unwind_Exception AFAICT.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All _Unwind_DeleteException does it invoke the call the exception_cleanup callback in _Unwind_Exception. Since the exception is our own, we can just free the Box directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm flagging this as temporarily unresolved to leave a comment here too since this was a question I had. Could a comment be left indicating that we're explicitly not calling _Unwind_DeleteException? Also, do you have a link to the source that it only calls the internal callback?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for _Unwind_DeleteException explicitly says that this function is for foreign exception that the runtime does not know how to free.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sure sorry I'm not questioning the validity of this deletion, just un-resolving it so my request for a comment in the code didn't show up as collapsed.

cause.unwrap()
let exception = Box::from_raw(ptr as *mut Exception);
exception.cause
}

// Rust's exception class identifier. This is used by personality routines to
Expand Down
7 changes: 7 additions & 0 deletions src/libpanic_unwind/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#![feature(staged_api)]
#![feature(std_internals)]
#![feature(unwind_attributes)]
#![feature(abi_thiscall)]
#![panic_runtime]
#![feature(panic_runtime)]

Expand Down Expand Up @@ -60,6 +61,12 @@ cfg_if::cfg_if! {
}
}

extern "C" {
/// Handler in libstd called when a panic object is dropped outside of
/// `catch_unwind`.
fn __rust_drop_panic() -> !;
}

mod dwarf;

// Entry point for catching an exception, implemented using the `try` intrinsic
Expand Down
65 changes: 58 additions & 7 deletions src/libpanic_unwind/seh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ use libc::{c_int, c_uint, c_void};
// #include <stdint.h>
//
// struct rust_panic {
// rust_panic(const rust_panic&);
// ~rust_panic();
//
// uint64_t x[2];
// }
// };
//
// void foo() {
// rust_panic a = {0, 1};
Expand Down Expand Up @@ -128,7 +131,7 @@ mod imp {
#[repr(C)]
pub struct _ThrowInfo {
pub attributes: c_uint,
pub pnfnUnwind: imp::ptr_t,
pub pmfnUnwind: imp::ptr_t,
pub pForwardCompat: imp::ptr_t,
pub pCatchableTypeArray: imp::ptr_t,
}
Expand All @@ -145,7 +148,7 @@ pub struct _CatchableType {
pub pType: imp::ptr_t,
pub thisDisplacement: _PMD,
pub sizeOrOffset: c_int,
pub copy_function: imp::ptr_t,
pub copyFunction: imp::ptr_t,
}

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

static mut THROW_INFO: _ThrowInfo = _ThrowInfo {
attributes: 0,
pnfnUnwind: ptr!(0),
pmfnUnwind: ptr!(0),
pForwardCompat: ptr!(0),
pCatchableTypeArray: ptr!(0),
};
Expand All @@ -181,7 +184,7 @@ static mut CATCHABLE_TYPE: _CatchableType = _CatchableType {
pType: ptr!(0),
thisDisplacement: _PMD { mdisp: 0, pdisp: -1, vdisp: 0 },
sizeOrOffset: mem::size_of::<[u64; 2]>() as c_int,
copy_function: ptr!(0),
copyFunction: ptr!(0),
};

extern "C" {
Expand All @@ -208,6 +211,43 @@ static mut TYPE_DESCRIPTOR: _TypeDescriptor = _TypeDescriptor {
name: TYPE_NAME,
};

// Destructor used if the C++ code decides to capture the exception and drop it
// without propagating it. The catch part of the try intrinsic will set the
// first word of the exception object to 0 so that it is skipped by the
// destructor.
//
// Note that x86 Windows uses the "thiscall" calling convention for C++ member
// functions instead of the default "C" calling convention.
//
// The exception_copy function is a bit special here: it is invoked by the MSVC
// runtime under a try/catch block and the panic that we generate here will be
// used as the result of the exception copy. This is used by the C++ runtime to
// support capturing exceptions with std::exception_ptr, which we can't support
// because Box<dyn Any> isn't clonable.
macro_rules! define_cleanup {
($abi:tt) => {
unsafe extern $abi fn exception_cleanup(e: *mut [u64; 2]) {
if (*e)[0] != 0 {
cleanup(*e);
super::__rust_drop_panic();
}
}
#[unwind(allowed)]
unsafe extern $abi fn exception_copy(_dest: *mut [u64; 2],
_src: *mut [u64; 2])
-> *mut [u64; 2] {
panic!("Rust panics cannot be copied");
}
}
}
cfg_if::cfg_if! {
if #[cfg(target_arch = "x86")] {
define_cleanup!("thiscall");
} else {
define_cleanup!("C");
}
}

pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
use core::intrinsics::atomic_store;

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

// This... may seems surprising, and justifiably so. On 32-bit MSVC the
// pointers between these structure are just that, pointers. On 64-bit MSVC,
Expand All @@ -243,6 +282,12 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
//
// In any case, we basically need to do something like this until we can
// express more operations in statics (and we may never be able to).
if !cfg!(bootstrap) {
atomic_store(
&mut THROW_INFO.pmfnUnwind as *mut _ as *mut u32,
ptr!(exception_cleanup) as u32,
);
}
atomic_store(
&mut THROW_INFO.pCatchableTypeArray as *mut _ as *mut u32,
ptr!(&CATCHABLE_TYPE_ARRAY as *const _) as u32,
Expand All @@ -255,6 +300,12 @@ pub unsafe fn panic(data: Box<dyn Any + Send>) -> u32 {
&mut CATCHABLE_TYPE.pType as *mut _ as *mut u32,
ptr!(&TYPE_DESCRIPTOR as *const _) as u32,
);
if !cfg!(bootstrap) {
atomic_store(
&mut CATCHABLE_TYPE.copyFunction as *mut _ as *mut u32,
ptr!(exception_copy) as u32,
);
}

extern "system" {
#[unwind(allowed)]
Expand Down
31 changes: 26 additions & 5 deletions src/librustc_codegen_llvm/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,40 +922,61 @@ fn codegen_msvc_try(
// #include <stdint.h>
//
// struct rust_panic {
// rust_panic(const rust_panic&);
// ~rust_panic();
//
// uint64_t x[2];
// }
//
// int bar(void (*foo)(void), uint64_t *ret) {
// try {
// foo();
// return 0;
// } catch(rust_panic a) {
// } catch(rust_panic& a) {
// ret[0] = a.x[0];
// ret[1] = a.x[1];
// a.x[0] = 0;
// return 1;
// }
// }
//
// More information can be found in libstd's seh.rs implementation.
let i64_2 = bx.type_array(bx.type_i64(), 2);
let i64_align = bx.tcx().data_layout.i64_align.abi;
let slot = bx.alloca(i64_2, i64_align);
let i64_2_ptr = bx.type_ptr_to(i64_2);
let ptr_align = bx.tcx().data_layout.pointer_align.abi;
let slot = bx.alloca(i64_2_ptr, ptr_align);
bx.invoke(func, &[data], normal.llbb(), catchswitch.llbb(), None);

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

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

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

let payload = catchpad.load(slot, i64_align);
let i64_align = bx.tcx().data_layout.i64_align.abi;
let payload_ptr = catchpad.load(slot, ptr_align);
let payload = catchpad.load(payload_ptr, i64_align);
let local_ptr = catchpad.bitcast(local_ptr, bx.type_ptr_to(i64_2));
catchpad.store(payload, local_ptr, i64_align);

// Clear the first word of the exception so avoid double-dropping it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I understand what's going on here, we catch the exception by reference now which means that the system unwinder will always invoke the destructor function listed. For us that destructor function will skip its actual work since the first slot will be set to zero (as configured here).

If that's true, how come we're catching by reference? Why not continue to catch by-value and just skip the destructor for Rust exceptions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we catch by value then C++ will give the catch a copy (not a move) of the exception object. It will still call the exception destructor after the catch ends. Catching by reference avoids this copy (which won't work anyways since our copy constructor panics).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, makes sense. Does C++ never catch by value or in any way try to invoke the copy constructor?

Also, can you inline these words into the code comments as well?

// This will be read by the destructor which is implicitly called at the
// end of the catch block by the runtime.
let payload_0_ptr = catchpad.inbounds_gep(payload_ptr, &[bx.const_i32(0), bx.const_i32(0)]);
catchpad.store(bx.const_u64(0), payload_0_ptr, i64_align);

catchpad.catch_ret(&funclet, caught.llbb());

caught.ret(bx.const_i32(1));
Expand Down
9 changes: 9 additions & 0 deletions src/libstd/panicking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ extern "C" {
fn __rust_start_panic(payload: usize) -> u32;
}

/// This function is called by the panic runtime if FFI code catches a Rust
/// panic but doesn't rethrow it. We don't support this case since it messes
/// with our panic count.
#[cfg(not(test))]
#[rustc_std_internal_symbol]
extern "C" fn __rust_drop_panic() -> ! {
rtabort!("Rust panics must be rethrown");
}

#[derive(Copy, Clone)]
enum Hook {
Default,
Expand Down
1 change: 0 additions & 1 deletion src/test/run-make-fulldeps/foreign-exceptions/foo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

// For linking libstdc++ on MinGW
#![cfg_attr(all(windows, target_env = "gnu"), feature(static_nobundle))]

#![feature(unwind_attributes)]

use std::panic::{catch_unwind, AssertUnwindSafe};
Expand Down