Skip to content

Commit

Permalink
Auto merge of #67711 - Amanieu:fix_unwind_leak, r=alexcrichton
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bors committed Jan 8, 2020
2 parents 7e393b5 + 5b41d99 commit f2e217f
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 27 deletions.
30 changes: 22 additions & 8 deletions src/libpanic_unwind/emcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,36 @@ 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>>,
}

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);
if exception == ptr::null_mut() {
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);

extern "C" fn exception_cleanup(ptr: *mut libc::c_void) {
unsafe {
ptr::drop_in_place(ptr as *mut Exception);
}
}
}

#[lang = "eh_personality"]
Expand All @@ -89,7 +103,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),
) -> !;
fn __gxx_personality_v0(
version: c_int,
Expand Down
10 changes: 4 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 @@ -87,10 +87,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 _);
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
1 change: 1 addition & 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
64 changes: 57 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,42 @@ 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);
}
}
#[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 +259,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 +281,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 +299,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.
// 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
17 changes: 17 additions & 0 deletions src/test/run-make-fulldeps/foreign-exceptions/foo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,21 @@ extern "C" {
throw;
}
}

void swallow_exception(void (*cb)()) {
try {
// Do a rethrow to ensure that the exception is only dropped once.
// This is necessary since we don't support copying exceptions.
try {
cb();
} catch (...) {
println("rethrowing Rust panic");
throw;
};
} catch (rust_panic e) {
assert(false && "shouldn't be able to catch a rust panic");
} catch (...) {
println("swallowing foreign exception in catch (...)");
}
}
}
30 changes: 29 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 All @@ -20,6 +19,8 @@ impl<'a> Drop for DropCheck<'a> {
extern "C" {
fn throw_cxx_exception();

fn swallow_exception(cb: extern "C" fn());

#[unwind(allowed)]
fn cxx_catch_callback(cb: extern "C" fn(), ok: *mut bool);
}
Expand Down Expand Up @@ -60,7 +61,34 @@ fn throw_rust_panic() {
assert!(cxx_ok);
}

fn check_exception_drop() {
static mut DROP_COUNT: usize = 0;

struct CountDrop;
impl Drop for CountDrop {
fn drop(&mut self) {
println!("CountDrop::drop");
unsafe {
DROP_COUNT += 1;
}
}
}


#[unwind(allowed)]
extern "C" fn callback() {
println!("throwing rust panic #2");
panic!(CountDrop);
}

unsafe {
swallow_exception(callback);
assert_eq!(DROP_COUNT, 1);
}
}

fn main() {
unsafe { throw_cxx_exception() };
throw_rust_panic();
check_exception_drop();
}

0 comments on commit f2e217f

Please sign in to comment.