Skip to content

Commit 4434036

Browse files
committed
Support unwinding after a panic
Fixes #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.
1 parent f6fef3b commit 4434036

File tree

5 files changed

+432
-27
lines changed

5 files changed

+432
-27
lines changed

src/fn_call.rs

Lines changed: 307 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use rustc::ty;
22
use rustc::ty::layout::{Align, LayoutOf, Size};
3-
use rustc::hir::def_id::DefId;
3+
use rustc::ty::InstanceDef;
4+
use rustc_target::spec::PanicStrategy;
45
use rustc::mir;
56
use syntax::attr;
67

@@ -18,10 +19,15 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
1819
ret: Option<mir::BasicBlock>,
1920
) -> EvalResult<'tcx, Option<&'mir mir::Mir<'tcx>>> {
2021
let this = self.eval_context_mut();
21-
trace!("eval_fn_call: {:#?}, {:?}", instance, dest.map(|place| *place));
22+
trace!("eval_fn_call: {:#?}, {:?} {:?}", instance, instance.def_id(), dest.map(|place| *place));
2223

2324
// First, run the common hooks also supported by CTFE.
24-
if this.hook_fn(instance, args, dest)? {
25+
// We *don't* forward panic-related items to the common hooks,
26+
// as we want to handle those specially
27+
if Some(instance.def_id()) != this.tcx.lang_items().panic_fn() &&
28+
Some(instance.def_id()) != this.tcx.lang_items().begin_panic_fn() &&
29+
this.hook_fn(instance, args, dest)? {
30+
2531
this.goto_block(ret)?;
2632
return Ok(None);
2733
}
@@ -39,11 +45,9 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
3945

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

4953
// Otherwise, load the MIR.
@@ -54,11 +58,12 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
5458
/// This function will handle `goto_block` if needed.
5559
fn emulate_foreign_item(
5660
&mut self,
57-
def_id: DefId,
61+
instance: ty::Instance<'tcx>,
5862
args: &[OpTy<'tcx, Borrow>],
5963
dest: Option<PlaceTy<'tcx, Borrow>>,
6064
ret: Option<mir::BasicBlock>,
61-
) -> EvalResult<'tcx> {
65+
) -> EvalResult<'tcx, Option<&'mir mir::Mir<'tcx>>> {
66+
let def_id = instance.def_id();
6267
let this = self.eval_context_mut();
6368
let attrs = this.tcx.get_attrs(def_id);
6469
let link_name = match attr::first_attr_value_str_by_name(&attrs, "link_name") {
@@ -71,8 +76,195 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
7176

7277
// First: functions that could diverge.
7378
match link_name {
74-
"__rust_start_panic" | "panic_impl" => {
75-
return err!(MachineError("the evaluated program panicked".to_string()));
79+
"panic_impl" => {
80+
// Manually forward to 'panic_impl' lang item
81+
let panic_impl_real = this.tcx.lang_items().panic_impl().unwrap();
82+
83+
return Ok(Some(this.load_mir(InstanceDef::Item(panic_impl_real))?));
84+
},
85+
"__rust_start_panic" => {
86+
// This function has the signature:
87+
// 'fn __rust_start_panic(payload: usize) -> u32;'
88+
//
89+
// The caller constructs 'payload' as follows
90+
// 1. We start with a type implementing core::panic::BoxMeUp
91+
// 2. We make this type into a trait object, obtaining a '&mut dyn BoxMeUp'
92+
// 3. We obtain a raw pointer to the above mutable reference: that is, we make:
93+
// '*mut &mut dyn BoxMeUp'
94+
// 4. We convert the raw pointer to a 'usize'
95+
//
96+
97+
// When a panic occurs, we (carefully!) reverse the above steps
98+
// to get back to the actual panic payload
99+
//
100+
// Even though our argument is a 'usize', Miri will have kept track
101+
// of the fact that it was created via a cast from a pointer.
102+
// This allows us to construct an ImmTy with the proper layout,
103+
// and dereference it
104+
//
105+
// Reference:
106+
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L101
107+
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L81
108+
//
109+
// payload_raw now represents our '&mut dyn BoxMeUp' - a fat pointer
110+
//
111+
// Note that we intentially call deref_operand before checking
112+
// This ensures that we always check the validity of the argument,
113+
// even if we don't end up using it
114+
115+
trace!("__rustc_start_panic: {:?}", this.frame().span);
116+
117+
118+
// Read our 'usize' payload argument (which was made by casting
119+
// a '*mut &mut dyn BoxMeUp'
120+
let payload_raw = this.read_scalar(args[0])?.not_undef()?;
121+
122+
// Construct an ImmTy, using the precomputed layout of '*mut &mut dyn BoxMeUp'
123+
let imm_ty = ImmTy::from_scalar(
124+
payload_raw,
125+
this.machine.cached_data.as_ref().unwrap().box_me_up_layout
126+
);
127+
128+
// Convert our ImmTy to an MPlace, and read it
129+
let mplace = this.ref_to_mplace(imm_ty)?;
130+
131+
// This is an '&mut dyn BoxMeUp'
132+
let payload_dyn = this.read_immediate(mplace.into())?;
133+
134+
// We deliberately do this after we do some validation of the
135+
// 'payload'. This should help catch some basic errors in
136+
// the caller of this function, even in abort mode
137+
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Abort {
138+
return err!(MachineError("the evaluated program abort-panicked".to_string()));
139+
}
140+
141+
// This part is tricky - we need to call BoxMeUp::box_me_up
142+
// on the vtable.
143+
//
144+
// core::panic::BoxMeUp is declared as follows:
145+
//
146+
// pub unsafe trait BoxMeUp {
147+
// fn box_me_up(&mut self) -> *mut (dyn Any + Send);
148+
// fn get(&mut self) -> &(dyn Any + Send);
149+
// }
150+
//
151+
// box_me_up is the first method in the vtable.
152+
// First, we extract the vtable pointer from our fat pointer,
153+
// and check its alignment
154+
155+
let vtable_ptr = payload_dyn.to_meta()?.expect("Expected fat pointer!").to_ptr()?;
156+
let data_ptr = payload_dyn.to_scalar_ptr()?;
157+
this.memory().check_align(vtable_ptr.into(), this.tcx.data_layout.pointer_align.abi)?;
158+
159+
// Now, we derefernce the vtable pointer.
160+
let alloc = this.memory().get(vtable_ptr.alloc_id)?;
161+
162+
// Finally, we extract the pointer to 'box_me_up'.
163+
// The vtable is layed out in memory like this:
164+
//
165+
//```
166+
// <drop_ptr> (usize)
167+
// <size> (usize)
168+
// <align> (usize)
169+
// <method_ptr_1> (usize)
170+
// <method_ptr_2> (usize)
171+
// ...
172+
// <method_ptr_n> (usize)
173+
//```
174+
//
175+
// Since box_me_up is the first method pointer
176+
// in the vtable, we use an offset of 3 pointer sizes
177+
// (skipping over <drop_ptr>, <size>, and <align>)
178+
179+
let box_me_up_ptr = alloc.read_ptr_sized(
180+
this,
181+
vtable_ptr.offset(this.pointer_size() * 3, this)?
182+
)?.to_ptr()?;
183+
184+
// Get the actual function instance
185+
let box_me_up_fn = this.memory().get_fn(box_me_up_ptr)?;
186+
let box_me_up_mir = this.load_mir(box_me_up_fn.def)?;
187+
188+
// Extract the signature
189+
// We know that there are no HRBTs here, so it's fine to use
190+
// skip_binder
191+
let fn_sig_temp = box_me_up_fn.ty(*this.tcx).fn_sig(*this.tcx);
192+
let fn_sig = fn_sig_temp.skip_binder();
193+
194+
// This is the layout of '*mut (dyn Any + Send)', which
195+
// is the return type of 'box_me_up'
196+
let dyn_ptr_layout = this.layout_of(fn_sig.output())?;
197+
198+
// We allocate space to store the return value of box_me_up:
199+
// '*mut (dyn Any + Send)', which is a fat
200+
201+
let temp_ptr = this.allocate(dyn_ptr_layout, MiriMemoryKind::UnwindHelper.into());
202+
203+
// Keep track of our current frame
204+
// This allows us to step throgh the exection of 'box_me_up',
205+
// exiting when we get back to this frame
206+
let cur_frame = this.cur_frame();
207+
208+
this.push_stack_frame(
209+
box_me_up_fn,
210+
box_me_up_mir.span,
211+
box_me_up_mir,
212+
Some(temp_ptr.into()),
213+
StackPopCleanup::None { cleanup: true }
214+
)?;
215+
216+
let mut args = this.frame().mir.args_iter();
217+
let arg_0 = this.eval_place(&mir::Place::Base(mir::PlaceBase::Local(args.next().unwrap())))?;
218+
this.write_scalar(data_ptr, arg_0)?;
219+
220+
// Step through execution of 'box_me_up'
221+
// We know that we're finished when our stack depth
222+
// returns to where it was before.
223+
//
224+
// Note that everything will get completely screwed up
225+
// if 'box_me_up' panics. This is fine, since this
226+
// function should never panic, as it's part of the core
227+
// panic handling infrastructure
228+
//
229+
// Normally, we would just let Miri drive
230+
// the execution of this stack frame.
231+
// However, we need to access its return value
232+
// in order to properly unwind.
233+
//
234+
// When we 'return' from '__rustc_start_panic',
235+
// we need to be executing the panic catch handler.
236+
// Therefore, we take care all all of the unwinding logic
237+
// here, instead of letting the Miri main loop do it
238+
while this.cur_frame() != cur_frame {
239+
this.step()?;
240+
}
241+
242+
// 'box_me_up' has finished. 'temp_ptr' now holds
243+
// a '*mut (dyn Any + Send)'
244+
// We want to split this into its consituient parts -
245+
// the data and vtable pointers - and store them back
246+
// into the panic handler frame
247+
let real_ret = this.read_immediate(temp_ptr.into())?;
248+
let real_ret_data = real_ret.to_scalar_ptr()?;
249+
let real_ret_vtable = real_ret.to_meta()?.expect("Expected fat pointer");
250+
251+
// We're in panic unwind mode. We pop off stack
252+
// frames until one of two things happens: we reach
253+
// a frame with 'catch_panic' set, or we pop of all frames
254+
//
255+
// If we pop off all frames without encountering 'catch_panic',
256+
// we exut.
257+
//
258+
// If we encounter 'catch_panic', we continue execution at that
259+
// frame, filling in data from the panic
260+
//
261+
unwind_stack(this, real_ret_data, real_ret_vtable)?;
262+
263+
this.memory_mut().deallocate(temp_ptr.to_ptr()?, None, MiriMemoryKind::UnwindHelper.into())?;
264+
this.dump_place(*dest.expect("dest is None!"));
265+
266+
return Ok(None)
267+
76268
}
77269
_ => if dest.is_none() {
78270
return err!(Unimplemented(
@@ -274,13 +466,27 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
274466
// data_ptr: *mut usize,
275467
// vtable_ptr: *mut usize,
276468
// ) -> u32
277-
// We abort on panic, so not much is going on here, but we still have to call the closure.
278469
let f = this.read_scalar(args[0])?.to_ptr()?;
279470
let data = this.read_scalar(args[1])?.not_undef()?;
471+
let data_ptr = this.deref_operand(args[2])?;
472+
let vtable_ptr = this.deref_operand(args[3])?;
280473
let f_instance = this.memory().get_fn(f)?;
281474
this.write_null(dest)?;
282475
trace!("__rust_maybe_catch_panic: {:?}", f_instance);
283476

477+
// In unwind mode, we tag this frame with some extra data.
478+
// This lets '__rust_start_panic' know that it should jump back
479+
// to this frame is a panic occurs.
480+
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Unwind {
481+
this.frame_mut().extra.catch_panic = Some(UnwindData {
482+
data: data.to_ptr()?,
483+
data_ptr,
484+
vtable_ptr,
485+
dest: dest.clone(),
486+
ret
487+
})
488+
}
489+
284490
// Now we make a function call.
285491
// TODO: consider making this reusable? `InterpretCx::step` does something similar
286492
// for the TLS destructors, and of course `eval_main`.
@@ -294,6 +500,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
294500
// Directly return to caller.
295501
StackPopCleanup::Goto(Some(ret)),
296502
)?;
503+
297504
let mut args = this.frame().mir.args_iter();
298505

299506
let arg_local = args.next().ok_or_else(||
@@ -311,7 +518,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
311518
this.write_null(dest)?;
312519

313520
// Don't fall through, we do *not* want to `goto_block`!
314-
return Ok(());
521+
return Ok(None);
315522
}
316523

317524
"memcmp" => {
@@ -786,7 +993,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
786993

787994
this.goto_block(Some(ret))?;
788995
this.dump_place(*dest);
789-
Ok(())
996+
Ok(None)
790997
}
791998

792999
fn write_null(&mut self, dest: PlaceTy<'tcx, Borrow>) -> EvalResult<'tcx> {
@@ -831,3 +1038,89 @@ fn gen_random<'a, 'mir, 'tcx>(
8311038
}
8321039
}
8331040
}
1041+
1042+
/// A helper method to unwind the stack.
1043+
///
1044+
/// We execute the 'unwind' blocks associated with frame
1045+
/// terminators as we go along (these blocks are responsible
1046+
/// for dropping frame locals in the event of a panic)
1047+
///
1048+
/// When we find our target frame, we write the panic payload
1049+
/// directly into its locals, and jump to it.
1050+
/// After that, panic handling is done - from the perspective
1051+
/// of the caller of '__rust_maybe_catch_panic', the function
1052+
/// has 'returned' normally, after which point Miri excecution
1053+
/// can proceeed normally.
1054+
fn unwind_stack<'a, 'mir, 'tcx>(
1055+
this: &mut MiriEvalContext<'a, 'mir, 'tcx>,
1056+
payload_data_ptr: Scalar<Borrow>,
1057+
payload_vtable_ptr: Scalar<Borrow>
1058+
) -> EvalResult<'tcx> {
1059+
1060+
let mut found = false;
1061+
1062+
while !this.stack().is_empty() {
1063+
// When '__rust_maybe_catch_panic' is called, it marks is frame
1064+
// with 'catch_panic'. When we find this marker, we've found
1065+
// our target frame to jump to.
1066+
if let Some(unwind_data) = this.frame_mut().extra.catch_panic.take() {
1067+
trace!("unwinding: found target frame: {:?}", this.frame().span);
1068+
1069+
let data_ptr = unwind_data.data_ptr.clone();
1070+
let vtable_ptr = unwind_data.vtable_ptr.clone();
1071+
let dest = unwind_data.dest.clone();
1072+
let ret = unwind_data.ret.clone();
1073+
drop(unwind_data);
1074+
1075+
1076+
// Here, we write directly into the frame of the function
1077+
// that called '__rust_maybe_catch_panic'.
1078+
// (NOT the function that called '__rust_start_panic')
1079+
1080+
this.write_scalar(payload_data_ptr, data_ptr.into())?;
1081+
this.write_scalar(payload_vtable_ptr, vtable_ptr.into())?;
1082+
1083+
// We 'return' the value 1 from __rust_maybe_catch_panic,
1084+
// since there was a panic
1085+
this.write_scalar(Scalar::from_int(1, dest.layout.size), dest)?;
1086+
1087+
// We're done - continue execution in the frame of the function
1088+
// that called '__rust_maybe_catch_panic,'
1089+
this.goto_block(Some(ret))?;
1090+
found = true;
1091+
1092+
break;
1093+
} else {
1094+
// This frame is above our target frame on the call stack.
1095+
// We pop it off the stack, running its 'unwind' block if applicable
1096+
trace!("unwinding: popping frame: {:?}", this.frame().span);
1097+
let block = &this.frame().mir.basic_blocks()[this.frame().block];
1098+
1099+
// All frames in the call stack should be executing their terminators.,
1100+
// as that's the only way for a basic block to perform a function call
1101+
if let Some(stmt) = block.statements.get(this.frame().stmt) {
1102+
panic!("Unexpcted statement '{:?}' for frame {:?}", stmt, this.frame().span);
1103+
}
1104+
1105+
// We're only interested in terminator types which allow for a cleanuup
1106+
// block (e.g. Call), and that also actually provide one
1107+
if let Some(Some(unwind)) = block.terminator().unwind() {
1108+
this.goto_block(Some(*unwind))?;
1109+
1110+
// Run the 'unwind' block until we encounter
1111+
// a 'Resume', which indicates that the block
1112+
// is done.
1113+
assert_eq!(this.run()?, StepOutcome::Resume);
1114+
}
1115+
1116+
// Pop this frame, and continue on to the next one
1117+
this.pop_stack_frame_unwind()?;
1118+
}
1119+
}
1120+
1121+
if !found {
1122+
// The 'start_fn' lang item should always install a panic handler
1123+
return err!(Unreachable);
1124+
}
1125+
return Ok(())
1126+
}

0 commit comments

Comments
 (0)