Skip to content

Commit

Permalink
Support unwinding after a panic
Browse files Browse the repository at this point in the history
Fixes rust-lang#658

This commit adds support for unwinding after a panic. It requires a
companion rustc PR to be merged, in order for the necessary hooks to
work properly.

Currently implemented:
* Selecting between unwind/abort mode based on the rustc Session
* Properly popping off stack frames, unwinding back the caller
* Running 'unwind' blocks in Mir terminators

Not yet implemented:
* 'Abort' terminators

This PR was getting fairly large, so I decided to open it for review without
implementing 'Abort' terminator support. This could either be added on
to this PR, or merged separately.
  • Loading branch information
Aaron1011 committed May 28, 2019
1 parent 4e329eb commit 59364c7
Show file tree
Hide file tree
Showing 5 changed files with 433 additions and 27 deletions.
323 changes: 308 additions & 15 deletions src/fn_call.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use rustc::ty;
use rustc::ty::layout::{Align, LayoutOf, Size};
use rustc::hir::def_id::DefId;
use rustc::ty::InstanceDef;
use rustc_target::spec::PanicStrategy;
use rustc::mir;
use syntax::attr;
use syntax::symbol::sym;
Expand All @@ -19,10 +20,15 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
ret: Option<mir::BasicBlock>,
) -> EvalResult<'tcx, Option<&'mir mir::Mir<'tcx>>> {
let this = self.eval_context_mut();
trace!("eval_fn_call: {:#?}, {:?}", instance, dest.map(|place| *place));
trace!("eval_fn_call: {:#?}, {:?} {:?}", instance, instance.def_id(), dest.map(|place| *place));

// First, run the common hooks also supported by CTFE.
if this.hook_fn(instance, args, dest)? {
// We *don't* forward panic-related items to the common hooks,
// as we want to handle those specially
if Some(instance.def_id()) != this.tcx.lang_items().panic_fn() &&
Some(instance.def_id()) != this.tcx.lang_items().begin_panic_fn() &&
this.hook_fn(instance, args, dest)? {

this.goto_block(ret)?;
return Ok(None);
}
Expand All @@ -40,11 +46,9 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'

// Try to see if we can do something about foreign items.
if this.tcx.is_foreign_item(instance.def_id()) {
// An external function that we cannot find MIR for, but we can still run enough
// An external function that we (possibly) cannot find MIR for, but we can still run enough
// of them to make miri viable.
this.emulate_foreign_item(instance.def_id(), args, dest, ret)?;
// `goto_block` already handled.
return Ok(None);
return Ok(this.emulate_foreign_item(instance, args, dest, ret)?);
}

// Otherwise, load the MIR.
Expand Down Expand Up @@ -135,11 +139,12 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
/// This function will handle `goto_block` if needed.
fn emulate_foreign_item(
&mut self,
def_id: DefId,
args: &[OpTy<'tcx, Tag>],
instance: ty::Instance<'tcx>,
args: &[OpTy<'tcx, Borrow>],
dest: Option<PlaceTy<'tcx, Tag>>,
ret: Option<mir::BasicBlock>,
) -> EvalResult<'tcx> {
) -> EvalResult<'tcx, Option<&'mir mir::Mir<'tcx>>> {
let def_id = instance.def_id();
let this = self.eval_context_mut();
let attrs = this.tcx.get_attrs(def_id);
let link_name = match attr::first_attr_value_str_by_name(&attrs, sym::link_name) {
Expand All @@ -152,8 +157,195 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'

// First: functions that diverge.
match link_name {
"__rust_start_panic" | "panic_impl" => {
return err!(MachineError("the evaluated program panicked".to_string()));
"panic_impl" => {
// Manually forward to 'panic_impl' lang item
let panic_impl_real = this.tcx.lang_items().panic_impl().unwrap();

return Ok(Some(this.load_mir(InstanceDef::Item(panic_impl_real))?));
},
"__rust_start_panic" => {
// This function has the signature:
// 'fn __rust_start_panic(payload: usize) -> u32;'
//
// The caller constructs 'payload' as follows
// 1. We start with a type implementing core::panic::BoxMeUp
// 2. We make this type into a trait object, obtaining a '&mut dyn BoxMeUp'
// 3. We obtain a raw pointer to the above mutable reference: that is, we make:
// '*mut &mut dyn BoxMeUp'
// 4. We convert the raw pointer to a 'usize'
//

// When a panic occurs, we (carefully!) reverse the above steps
// to get back to the actual panic payload
//
// Even though our argument is a 'usize', Miri will have kept track
// of the fact that it was created via a cast from a pointer.
// This allows us to construct an ImmTy with the proper layout,
// and dereference it
//
// Reference:
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L101
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L81
//
// payload_raw now represents our '&mut dyn BoxMeUp' - a fat pointer
//
// Note that we intentially call deref_operand before checking
// This ensures that we always check the validity of the argument,
// even if we don't end up using it

trace!("__rustc_start_panic: {:?}", this.frame().span);


// Read our 'usize' payload argument (which was made by casting
// a '*mut &mut dyn BoxMeUp'
let payload_raw = this.read_scalar(args[0])?.not_undef()?;

// Construct an ImmTy, using the precomputed layout of '*mut &mut dyn BoxMeUp'
let imm_ty = ImmTy::from_scalar(
payload_raw,
this.machine.cached_data.as_ref().unwrap().box_me_up_layout
);

// Convert our ImmTy to an MPlace, and read it
let mplace = this.ref_to_mplace(imm_ty)?;

// This is an '&mut dyn BoxMeUp'
let payload_dyn = this.read_immediate(mplace.into())?;

// We deliberately do this after we do some validation of the
// 'payload'. This should help catch some basic errors in
// the caller of this function, even in abort mode
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Abort {
return err!(MachineError("the evaluated program abort-panicked".to_string()));
}

// This part is tricky - we need to call BoxMeUp::box_me_up
// on the vtable.
//
// core::panic::BoxMeUp is declared as follows:
//
// pub unsafe trait BoxMeUp {
// fn box_me_up(&mut self) -> *mut (dyn Any + Send);
// fn get(&mut self) -> &(dyn Any + Send);
// }
//
// box_me_up is the first method in the vtable.
// First, we extract the vtable pointer from our fat pointer,
// and check its alignment

let vtable_ptr = payload_dyn.to_meta()?.expect("Expected fat pointer!").to_ptr()?;
let data_ptr = payload_dyn.to_scalar_ptr()?;
this.memory().check_align(vtable_ptr.into(), this.tcx.data_layout.pointer_align.abi)?;

// Now, we derefernce the vtable pointer.
let alloc = this.memory().get(vtable_ptr.alloc_id)?;

// Finally, we extract the pointer to 'box_me_up'.
// The vtable is layed out in memory like this:
//
//```
// <drop_ptr> (usize)
// <size> (usize)
// <align> (usize)
// <method_ptr_1> (usize)
// <method_ptr_2> (usize)
// ...
// <method_ptr_n> (usize)
//```
//
// Since box_me_up is the first method pointer
// in the vtable, we use an offset of 3 pointer sizes
// (skipping over <drop_ptr>, <size>, and <align>)

let box_me_up_ptr = alloc.read_ptr_sized(
this,
vtable_ptr.offset(this.pointer_size() * 3, this)?
)?.to_ptr()?;

// Get the actual function instance
let box_me_up_fn = this.memory().get_fn(box_me_up_ptr)?;
let box_me_up_mir = this.load_mir(box_me_up_fn.def)?;

// Extract the signature
// We know that there are no HRBTs here, so it's fine to use
// skip_binder
let fn_sig_temp = box_me_up_fn.ty(*this.tcx).fn_sig(*this.tcx);
let fn_sig = fn_sig_temp.skip_binder();

// This is the layout of '*mut (dyn Any + Send)', which
// is the return type of 'box_me_up'
let dyn_ptr_layout = this.layout_of(fn_sig.output())?;

// We allocate space to store the return value of box_me_up:
// '*mut (dyn Any + Send)', which is a fat

let temp_ptr = this.allocate(dyn_ptr_layout, MiriMemoryKind::UnwindHelper.into());

// Keep track of our current frame
// This allows us to step throgh the exection of 'box_me_up',
// exiting when we get back to this frame
let cur_frame = this.cur_frame();

this.push_stack_frame(
box_me_up_fn,
box_me_up_mir.span,
box_me_up_mir,
Some(temp_ptr.into()),
StackPopCleanup::None { cleanup: true }
)?;

let mut args = this.frame().mir.args_iter();
let arg_0 = this.eval_place(&mir::Place::Base(mir::PlaceBase::Local(args.next().unwrap())))?;
this.write_scalar(data_ptr, arg_0)?;

// Step through execution of 'box_me_up'
// We know that we're finished when our stack depth
// returns to where it was before.
//
// Note that everything will get completely screwed up
// if 'box_me_up' panics. This is fine, since this
// function should never panic, as it's part of the core
// panic handling infrastructure
//
// Normally, we would just let Miri drive
// the execution of this stack frame.
// However, we need to access its return value
// in order to properly unwind.
//
// When we 'return' from '__rustc_start_panic',
// we need to be executing the panic catch handler.
// Therefore, we take care all all of the unwinding logic
// here, instead of letting the Miri main loop do it
while this.cur_frame() != cur_frame {
this.step()?;
}

// 'box_me_up' has finished. 'temp_ptr' now holds
// a '*mut (dyn Any + Send)'
// We want to split this into its consituient parts -
// the data and vtable pointers - and store them back
// into the panic handler frame
let real_ret = this.read_immediate(temp_ptr.into())?;
let real_ret_data = real_ret.to_scalar_ptr()?;
let real_ret_vtable = real_ret.to_meta()?.expect("Expected fat pointer");

// We're in panic unwind mode. We pop off stack
// frames until one of two things happens: we reach
// a frame with 'catch_panic' set, or we pop of all frames
//
// If we pop off all frames without encountering 'catch_panic',
// we exut.
//
// If we encounter 'catch_panic', we continue execution at that
// frame, filling in data from the panic
//
unwind_stack(this, real_ret_data, real_ret_vtable)?;

this.memory_mut().deallocate(temp_ptr.to_ptr()?, None, MiriMemoryKind::UnwindHelper.into())?;
this.dump_place(*dest.expect("dest is None!"));

return Ok(None)

}
"exit" | "ExitProcess" => {
// it's really u32 for ExitProcess, but we have to put it into the `Exit` error variant anyway
Expand Down Expand Up @@ -341,13 +533,27 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
// data_ptr: *mut usize,
// vtable_ptr: *mut usize,
// ) -> u32
// We abort on panic, so not much is going on here, but we still have to call the closure.
let f = this.read_scalar(args[0])?.to_ptr()?;
let data = this.read_scalar(args[1])?.not_undef()?;
let data_ptr = this.deref_operand(args[2])?;
let vtable_ptr = this.deref_operand(args[3])?;
let f_instance = this.memory().get_fn(f)?;
this.write_null(dest)?;
trace!("__rust_maybe_catch_panic: {:?}", f_instance);

// In unwind mode, we tag this frame with some extra data.
// This lets '__rust_start_panic' know that it should jump back
// to this frame is a panic occurs.
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Unwind {
this.frame_mut().extra.catch_panic = Some(UnwindData {
data: data.to_ptr()?,
data_ptr,
vtable_ptr,
dest: dest.clone(),
ret
})
}

// Now we make a function call.
// TODO: consider making this reusable? `InterpretCx::step` does something similar
// for the TLS destructors, and of course `eval_main`.
Expand All @@ -361,6 +567,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
// Directly return to caller.
StackPopCleanup::Goto(Some(ret)),
)?;

let mut args = this.frame().mir.args_iter();

let arg_local = args.next().ok_or_else(||
Expand All @@ -378,7 +585,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
this.write_null(dest)?;

// Don't fall through, we do *not* want to `goto_block`!
return Ok(());
return Ok(None);
}

"memcmp" => {
Expand Down Expand Up @@ -891,7 +1098,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'

this.goto_block(Some(ret))?;
this.dump_place(*dest);
Ok(())
Ok(None)
}

fn write_null(&mut self, dest: PlaceTy<'tcx, Tag>) -> EvalResult<'tcx> {
Expand Down Expand Up @@ -945,3 +1152,89 @@ fn gen_random<'a, 'mir, 'tcx>(
this.memory_mut().get_mut(ptr.alloc_id)?
.write_bytes(tcx, ptr, &data)
}

/// A helper method to unwind the stack.
///
/// We execute the 'unwind' blocks associated with frame
/// terminators as we go along (these blocks are responsible
/// for dropping frame locals in the event of a panic)
///
/// When we find our target frame, we write the panic payload
/// directly into its locals, and jump to it.
/// After that, panic handling is done - from the perspective
/// of the caller of '__rust_maybe_catch_panic', the function
/// has 'returned' normally, after which point Miri excecution
/// can proceeed normally.
fn unwind_stack<'a, 'mir, 'tcx>(
this: &mut MiriEvalContext<'a, 'mir, 'tcx>,
payload_data_ptr: Scalar<Borrow>,
payload_vtable_ptr: Scalar<Borrow>
) -> EvalResult<'tcx> {

let mut found = false;

while !this.stack().is_empty() {
// When '__rust_maybe_catch_panic' is called, it marks is frame
// with 'catch_panic'. When we find this marker, we've found
// our target frame to jump to.
if let Some(unwind_data) = this.frame_mut().extra.catch_panic.take() {
trace!("unwinding: found target frame: {:?}", this.frame().span);

let data_ptr = unwind_data.data_ptr.clone();
let vtable_ptr = unwind_data.vtable_ptr.clone();
let dest = unwind_data.dest.clone();
let ret = unwind_data.ret.clone();
drop(unwind_data);


// Here, we write directly into the frame of the function
// that called '__rust_maybe_catch_panic'.
// (NOT the function that called '__rust_start_panic')

this.write_scalar(payload_data_ptr, data_ptr.into())?;
this.write_scalar(payload_vtable_ptr, vtable_ptr.into())?;

// We 'return' the value 1 from __rust_maybe_catch_panic,
// since there was a panic
this.write_scalar(Scalar::from_int(1, dest.layout.size), dest)?;

// We're done - continue execution in the frame of the function
// that called '__rust_maybe_catch_panic,'
this.goto_block(Some(ret))?;
found = true;

break;
} else {
// This frame is above our target frame on the call stack.
// We pop it off the stack, running its 'unwind' block if applicable
trace!("unwinding: popping frame: {:?}", this.frame().span);
let block = &this.frame().mir.basic_blocks()[this.frame().block];

// All frames in the call stack should be executing their terminators.,
// as that's the only way for a basic block to perform a function call
if let Some(stmt) = block.statements.get(this.frame().stmt) {
panic!("Unexpcted statement '{:?}' for frame {:?}", stmt, this.frame().span);
}

// We're only interested in terminator types which allow for a cleanuup
// block (e.g. Call), and that also actually provide one
if let Some(Some(unwind)) = block.terminator().unwind() {
this.goto_block(Some(*unwind))?;

// Run the 'unwind' block until we encounter
// a 'Resume', which indicates that the block
// is done.
assert_eq!(this.run()?, StepOutcome::Resume);
}

// Pop this frame, and continue on to the next one
this.pop_stack_frame_unwind()?;
}
}

if !found {
// The 'start_fn' lang item should always install a panic handler
return err!(Unreachable);
}
return Ok(())
}
Loading

0 comments on commit 59364c7

Please sign in to comment.