Skip to content

Commit b3a1e12

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 048ce3c commit b3a1e12

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

@@ -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.
@@ -134,11 +138,12 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
134138
/// This function will handle `goto_block` if needed.
135139
fn emulate_foreign_item(
136140
&mut self,
137-
def_id: DefId,
138-
args: &[OpTy<'tcx, Tag>],
141+
instance: ty::Instance<'tcx>,
142+
args: &[OpTy<'tcx, Borrow>],
139143
dest: Option<PlaceTy<'tcx, Tag>>,
140144
ret: Option<mir::BasicBlock>,
141-
) -> EvalResult<'tcx> {
145+
) -> EvalResult<'tcx, Option<&'mir mir::Mir<'tcx>>> {
146+
let def_id = instance.def_id();
142147
let this = self.eval_context_mut();
143148
let attrs = this.tcx.get_attrs(def_id);
144149
let link_name = match attr::first_attr_value_str_by_name(&attrs, "link_name") {
@@ -151,8 +156,195 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
151156

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

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

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

379586
// Don't fall through, we do *not* want to `goto_block`!
380-
return Ok(());
587+
return Ok(None);
381588
}
382589

383590
"memcmp" => {
@@ -890,7 +1097,7 @@ pub trait EvalContextExt<'a, 'mir, 'tcx: 'a + 'mir>: crate::MiriEvalContextExt<'
8901097

8911098
this.goto_block(Some(ret))?;
8921099
this.dump_place(*dest);
893-
Ok(())
1100+
Ok(None)
8941101
}
8951102

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

0 commit comments

Comments
 (0)