Skip to content

Commit bbdbc78

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 9dfbebd commit bbdbc78

File tree

5 files changed

+433
-27
lines changed

5 files changed

+433
-27
lines changed

src/fn_call.rs

Lines changed: 308 additions & 15 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
use syntax::symbol::sym;
@@ -19,10 +20,15 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
1920
ret: Option<mir::BasicBlock>,
2021
) -> EvalResult<'tcx, Option<&'mir mir::Body<'tcx>>> {
2122
let this = self.eval_context_mut();
22-
trace!("eval_fn_call: {:#?}, {:?}", instance, dest.map(|place| *place));
23+
trace!("eval_fn_call: {:#?}, {:?} {:?}", instance, instance.def_id(), dest.map(|place| *place));
2324

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

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

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

153158
// First: functions that diverge.
154159
match link_name {
155-
"__rust_start_panic" | "panic_impl" => {
156-
return err!(MachineError("the evaluated program panicked".to_string()));
160+
"panic_impl" => {
161+
// Manually forward to 'panic_impl' lang item
162+
let panic_impl_real = this.tcx.lang_items().panic_impl().unwrap();
163+
164+
return Ok(Some(this.load_mir(InstanceDef::Item(panic_impl_real))?));
165+
},
166+
"__rust_start_panic" => {
167+
// This function has the signature:
168+
// 'fn __rust_start_panic(payload: usize) -> u32;'
169+
//
170+
// The caller constructs 'payload' as follows
171+
// 1. We start with a type implementing core::panic::BoxMeUp
172+
// 2. We make this type into a trait object, obtaining a '&mut dyn BoxMeUp'
173+
// 3. We obtain a raw pointer to the above mutable reference: that is, we make:
174+
// '*mut &mut dyn BoxMeUp'
175+
// 4. We convert the raw pointer to a 'usize'
176+
//
177+
178+
// When a panic occurs, we (carefully!) reverse the above steps
179+
// to get back to the actual panic payload
180+
//
181+
// Even though our argument is a 'usize', Miri will have kept track
182+
// of the fact that it was created via a cast from a pointer.
183+
// This allows us to construct an ImmTy with the proper layout,
184+
// and dereference it
185+
//
186+
// Reference:
187+
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L101
188+
// https://github.com/rust-lang/rust/blob/9ebf47851a357faa4cd97f4b1dc7835f6376e639/src/libpanic_unwind/lib.rs#L81
189+
//
190+
// payload_raw now represents our '&mut dyn BoxMeUp' - a fat pointer
191+
//
192+
// Note that we intentially call deref_operand before checking
193+
// This ensures that we always check the validity of the argument,
194+
// even if we don't end up using it
195+
196+
trace!("__rustc_start_panic: {:?}", this.frame().span);
197+
198+
199+
// Read our 'usize' payload argument (which was made by casting
200+
// a '*mut &mut dyn BoxMeUp'
201+
let payload_raw = this.read_scalar(args[0])?.not_undef()?;
202+
203+
// Construct an ImmTy, using the precomputed layout of '*mut &mut dyn BoxMeUp'
204+
let imm_ty = ImmTy::from_scalar(
205+
payload_raw,
206+
this.machine.cached_data.as_ref().unwrap().box_me_up_layout
207+
);
208+
209+
// Convert our ImmTy to an MPlace, and read it
210+
let mplace = this.ref_to_mplace(imm_ty)?;
211+
212+
// This is an '&mut dyn BoxMeUp'
213+
let payload_dyn = this.read_immediate(mplace.into())?;
214+
215+
// We deliberately do this after we do some validation of the
216+
// 'payload'. This should help catch some basic errors in
217+
// the caller of this function, even in abort mode
218+
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Abort {
219+
return err!(MachineError("the evaluated program abort-panicked".to_string()));
220+
}
221+
222+
// This part is tricky - we need to call BoxMeUp::box_me_up
223+
// on the vtable.
224+
//
225+
// core::panic::BoxMeUp is declared as follows:
226+
//
227+
// pub unsafe trait BoxMeUp {
228+
// fn box_me_up(&mut self) -> *mut (dyn Any + Send);
229+
// fn get(&mut self) -> &(dyn Any + Send);
230+
// }
231+
//
232+
// box_me_up is the first method in the vtable.
233+
// First, we extract the vtable pointer from our fat pointer,
234+
// and check its alignment
235+
236+
let vtable_ptr = payload_dyn.to_meta()?.expect("Expected fat pointer!").to_ptr()?;
237+
let data_ptr = payload_dyn.to_scalar_ptr()?;
238+
this.memory().check_align(vtable_ptr.into(), this.tcx.data_layout.pointer_align.abi)?;
239+
240+
// Now, we derefernce the vtable pointer.
241+
let alloc = this.memory().get(vtable_ptr.alloc_id)?;
242+
243+
// Finally, we extract the pointer to 'box_me_up'.
244+
// The vtable is layed out in memory like this:
245+
//
246+
//```
247+
// <drop_ptr> (usize)
248+
// <size> (usize)
249+
// <align> (usize)
250+
// <method_ptr_1> (usize)
251+
// <method_ptr_2> (usize)
252+
// ...
253+
// <method_ptr_n> (usize)
254+
//```
255+
//
256+
// Since box_me_up is the first method pointer
257+
// in the vtable, we use an offset of 3 pointer sizes
258+
// (skipping over <drop_ptr>, <size>, and <align>)
259+
260+
let box_me_up_ptr = alloc.read_ptr_sized(
261+
this,
262+
vtable_ptr.offset(this.pointer_size() * 3, this)?
263+
)?.to_ptr()?;
264+
265+
// Get the actual function instance
266+
let box_me_up_fn = this.memory().get_fn(box_me_up_ptr)?;
267+
let box_me_up_mir = this.load_mir(box_me_up_fn.def)?;
268+
269+
// Extract the signature
270+
// We know that there are no HRBTs here, so it's fine to use
271+
// skip_binder
272+
let fn_sig_temp = box_me_up_fn.ty(*this.tcx).fn_sig(*this.tcx);
273+
let fn_sig = fn_sig_temp.skip_binder();
274+
275+
// This is the layout of '*mut (dyn Any + Send)', which
276+
// is the return type of 'box_me_up'
277+
let dyn_ptr_layout = this.layout_of(fn_sig.output())?;
278+
279+
// We allocate space to store the return value of box_me_up:
280+
// '*mut (dyn Any + Send)', which is a fat
281+
282+
let temp_ptr = this.allocate(dyn_ptr_layout, MiriMemoryKind::UnwindHelper.into());
283+
284+
// Keep track of our current frame
285+
// This allows us to step throgh the exection of 'box_me_up',
286+
// exiting when we get back to this frame
287+
let cur_frame = this.cur_frame();
288+
289+
this.push_stack_frame(
290+
box_me_up_fn,
291+
box_me_up_mir.span,
292+
box_me_up_mir,
293+
Some(temp_ptr.into()),
294+
StackPopCleanup::None { cleanup: true }
295+
)?;
296+
297+
let mut args = this.frame().mir.args_iter();
298+
let arg_0 = this.eval_place(&mir::Place::Base(mir::PlaceBase::Local(args.next().unwrap())))?;
299+
this.write_scalar(data_ptr, arg_0)?;
300+
301+
// Step through execution of 'box_me_up'
302+
// We know that we're finished when our stack depth
303+
// returns to where it was before.
304+
//
305+
// Note that everything will get completely screwed up
306+
// if 'box_me_up' panics. This is fine, since this
307+
// function should never panic, as it's part of the core
308+
// panic handling infrastructure
309+
//
310+
// Normally, we would just let Miri drive
311+
// the execution of this stack frame.
312+
// However, we need to access its return value
313+
// in order to properly unwind.
314+
//
315+
// When we 'return' from '__rustc_start_panic',
316+
// we need to be executing the panic catch handler.
317+
// Therefore, we take care all all of the unwinding logic
318+
// here, instead of letting the Miri main loop do it
319+
while this.cur_frame() != cur_frame {
320+
this.step()?;
321+
}
322+
323+
// 'box_me_up' has finished. 'temp_ptr' now holds
324+
// a '*mut (dyn Any + Send)'
325+
// We want to split this into its consituient parts -
326+
// the data and vtable pointers - and store them back
327+
// into the panic handler frame
328+
let real_ret = this.read_immediate(temp_ptr.into())?;
329+
let real_ret_data = real_ret.to_scalar_ptr()?;
330+
let real_ret_vtable = real_ret.to_meta()?.expect("Expected fat pointer");
331+
332+
// We're in panic unwind mode. We pop off stack
333+
// frames until one of two things happens: we reach
334+
// a frame with 'catch_panic' set, or we pop of all frames
335+
//
336+
// If we pop off all frames without encountering 'catch_panic',
337+
// we exut.
338+
//
339+
// If we encounter 'catch_panic', we continue execution at that
340+
// frame, filling in data from the panic
341+
//
342+
unwind_stack(this, real_ret_data, real_ret_vtable)?;
343+
344+
this.memory_mut().deallocate(temp_ptr.to_ptr()?, None, MiriMemoryKind::UnwindHelper.into())?;
345+
this.dump_place(*dest.expect("dest is None!"));
346+
347+
return Ok(None)
348+
157349
}
158350
"exit" | "ExitProcess" => {
159351
// it's really u32 for ExitProcess, but we have to put it into the `Exit` error variant anyway
@@ -341,13 +533,27 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
341533
// data_ptr: *mut usize,
342534
// vtable_ptr: *mut usize,
343535
// ) -> u32
344-
// We abort on panic, so not much is going on here, but we still have to call the closure.
345536
let f = this.read_scalar(args[0])?.to_ptr()?;
346537
let data = this.read_scalar(args[1])?.not_undef()?;
538+
let data_ptr = this.deref_operand(args[2])?;
539+
let vtable_ptr = this.deref_operand(args[3])?;
347540
let f_instance = this.memory().get_fn(f)?;
348541
this.write_null(dest)?;
349542
trace!("__rust_maybe_catch_panic: {:?}", f_instance);
350543

544+
// In unwind mode, we tag this frame with some extra data.
545+
// This lets '__rust_start_panic' know that it should jump back
546+
// to this frame is a panic occurs.
547+
if this.tcx.tcx.sess.panic_strategy() == PanicStrategy::Unwind {
548+
this.frame_mut().extra.catch_panic = Some(UnwindData {
549+
data: data.to_ptr()?,
550+
data_ptr,
551+
vtable_ptr,
552+
dest: dest.clone(),
553+
ret
554+
})
555+
}
556+
351557
// Now we make a function call.
352558
// TODO: consider making this reusable? `InterpretCx::step` does something similar
353559
// for the TLS destructors, and of course `eval_main`.
@@ -361,6 +567,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
361567
// Directly return to caller.
362568
StackPopCleanup::Goto(Some(ret)),
363569
)?;
570+
364571
let mut args = this.frame().mir.args_iter();
365572

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

380587
// Don't fall through, we do *not* want to `goto_block`!
381-
return Ok(());
588+
return Ok(None);
382589
}
383590

384591
"memcmp" => {
@@ -892,7 +1099,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
8921099

8931100
this.goto_block(Some(ret))?;
8941101
this.dump_place(*dest);
895-
Ok(())
1102+
Ok(None)
8961103
}
8971104

8981105
fn write_null(&mut self, dest: PlaceTy<'tcx, Tag>) -> EvalResult<'tcx> {
@@ -946,3 +1153,89 @@ fn gen_random<'a, 'mir, 'tcx>(
9461153
this.memory_mut().get_mut(ptr.alloc_id)?
9471154
.write_bytes(tcx, ptr, &data)
9481155
}
1156+
1157+
/// A helper method to unwind the stack.
1158+
///
1159+
/// We execute the 'unwind' blocks associated with frame
1160+
/// terminators as we go along (these blocks are responsible
1161+
/// for dropping frame locals in the event of a panic)
1162+
///
1163+
/// When we find our target frame, we write the panic payload
1164+
/// directly into its locals, and jump to it.
1165+
/// After that, panic handling is done - from the perspective
1166+
/// of the caller of '__rust_maybe_catch_panic', the function
1167+
/// has 'returned' normally, after which point Miri excecution
1168+
/// can proceeed normally.
1169+
fn unwind_stack<'a, 'mir, 'tcx>(
1170+
this: &mut MiriEvalContext<'a, 'mir, 'tcx>,
1171+
payload_data_ptr: Scalar<Borrow>,
1172+
payload_vtable_ptr: Scalar<Borrow>
1173+
) -> EvalResult<'tcx> {
1174+
1175+
let mut found = false;
1176+
1177+
while !this.stack().is_empty() {
1178+
// When '__rust_maybe_catch_panic' is called, it marks is frame
1179+
// with 'catch_panic'. When we find this marker, we've found
1180+
// our target frame to jump to.
1181+
if let Some(unwind_data) = this.frame_mut().extra.catch_panic.take() {
1182+
trace!("unwinding: found target frame: {:?}", this.frame().span);
1183+
1184+
let data_ptr = unwind_data.data_ptr.clone();
1185+
let vtable_ptr = unwind_data.vtable_ptr.clone();
1186+
let dest = unwind_data.dest.clone();
1187+
let ret = unwind_data.ret.clone();
1188+
drop(unwind_data);
1189+
1190+
1191+
// Here, we write directly into the frame of the function
1192+
// that called '__rust_maybe_catch_panic'.
1193+
// (NOT the function that called '__rust_start_panic')
1194+
1195+
this.write_scalar(payload_data_ptr, data_ptr.into())?;
1196+
this.write_scalar(payload_vtable_ptr, vtable_ptr.into())?;
1197+
1198+
// We 'return' the value 1 from __rust_maybe_catch_panic,
1199+
// since there was a panic
1200+
this.write_scalar(Scalar::from_int(1, dest.layout.size), dest)?;
1201+
1202+
// We're done - continue execution in the frame of the function
1203+
// that called '__rust_maybe_catch_panic,'
1204+
this.goto_block(Some(ret))?;
1205+
found = true;
1206+
1207+
break;
1208+
} else {
1209+
// This frame is above our target frame on the call stack.
1210+
// We pop it off the stack, running its 'unwind' block if applicable
1211+
trace!("unwinding: popping frame: {:?}", this.frame().span);
1212+
let block = &this.frame().mir.basic_blocks()[this.frame().block];
1213+
1214+
// All frames in the call stack should be executing their terminators.,
1215+
// as that's the only way for a basic block to perform a function call
1216+
if let Some(stmt) = block.statements.get(this.frame().stmt) {
1217+
panic!("Unexpcted statement '{:?}' for frame {:?}", stmt, this.frame().span);
1218+
}
1219+
1220+
// We're only interested in terminator types which allow for a cleanuup
1221+
// block (e.g. Call), and that also actually provide one
1222+
if let Some(Some(unwind)) = block.terminator().unwind() {
1223+
this.goto_block(Some(*unwind))?;
1224+
1225+
// Run the 'unwind' block until we encounter
1226+
// a 'Resume', which indicates that the block
1227+
// is done.
1228+
assert_eq!(this.run()?, StepOutcome::Resume);
1229+
}
1230+
1231+
// Pop this frame, and continue on to the next one
1232+
this.pop_stack_frame_unwind()?;
1233+
}
1234+
}
1235+
1236+
if !found {
1237+
// The 'start_fn' lang item should always install a panic handler
1238+
return err!(Unreachable);
1239+
}
1240+
return Ok(())
1241+
}

0 commit comments

Comments
 (0)