Skip to content
This repository was archived by the owner on Mar 24, 2022. It is now read-only.

Commit 2559743

Browse files
test and support forced unwinding of guests stopped from stack overflows
1 parent 6557e8e commit 2559743

File tree

9 files changed

+245
-82
lines changed

9 files changed

+245
-82
lines changed

lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub struct Slot {
8282
pub limits: Limits,
8383

8484
pub region: Weak<dyn RegionInternal>,
85+
86+
pub(crate) redzone_stack_enabled: bool,
8587
}
8688

8789
// raw pointers require unsafe impl
@@ -92,6 +94,14 @@ impl Slot {
9294
pub fn stack_top(&self) -> *mut c_void {
9395
(self.stack as usize + self.limits.stack_size) as *mut c_void
9496
}
97+
98+
pub fn stack_redzone_start(&self) -> *mut c_void {
99+
(self.stack as usize - self.stack_redzone_size()) as *mut c_void
100+
}
101+
102+
pub fn stack_redzone_size(&self) -> usize {
103+
host_page_size()
104+
}
95105
}
96106

97107
/// The structure that manages the allocations backing an `Instance`.
@@ -113,6 +123,24 @@ impl Drop for Alloc {
113123
}
114124

115125
impl Alloc {
126+
pub(crate) fn enable_stack_redzone(&mut self) {
127+
let slot = self
128+
.slot
129+
.as_mut()
130+
.expect("alloc has a Slot when toggling stack redzone");
131+
slot.redzone_stack_enabled = true;
132+
self.region.enable_stack_redzone(slot)
133+
}
134+
135+
pub(crate) fn disable_stack_redzone(&mut self) {
136+
let slot = self
137+
.slot
138+
.as_mut()
139+
.expect("alloc has a Slot when toggling stack redzone");
140+
slot.redzone_stack_enabled = false;
141+
self.region.disable_stack_redzone(slot)
142+
}
143+
116144
pub fn addr_in_heap_guard(&self, addr: *const c_void) -> bool {
117145
let heap = self.slot().heap as usize;
118146
let guard_start = heap + self.heap_accessible_size;
@@ -261,13 +289,41 @@ impl Alloc {
261289
std::slice::from_raw_parts_mut(self.slot().heap as *mut u64, self.heap_accessible_size / 8)
262290
}
263291

292+
pub(crate) fn stack_start(&self) -> *mut u8 {
293+
let mut stack_start = self.slot().stack as usize;
294+
295+
if self
296+
.slot
297+
.as_ref()
298+
.expect("alloc has a slot when we want to access its stack")
299+
.redzone_stack_enabled
300+
{
301+
stack_start -= host_page_size();
302+
}
303+
304+
stack_start as *mut u8
305+
}
306+
307+
pub(crate) fn stack_size(&self) -> usize {
308+
let mut stack_size = self.slot().limits.stack_size;
309+
if self
310+
.slot
311+
.as_ref()
312+
.expect("alloc has a slot when we want to access its stack")
313+
.redzone_stack_enabled
314+
{
315+
stack_size += host_page_size();
316+
}
317+
stack_size
318+
}
319+
264320
/// Return the stack as a mutable byte slice.
265321
///
266322
/// Since the stack grows down, `alloc.stack_mut()[0]` is the top of the stack, and
267323
/// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last byte at the bottom of the
268324
/// stack.
269325
pub unsafe fn stack_mut(&mut self) -> &mut [u8] {
270-
std::slice::from_raw_parts_mut(self.slot().stack as *mut u8, self.slot().limits.stack_size)
326+
std::slice::from_raw_parts_mut(self.stack_start(), self.stack_size())
271327
}
272328

273329
/// Return the stack as a mutable slice of 64-bit words.
@@ -276,18 +332,12 @@ impl Alloc {
276332
/// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last word at the bottom of the
277333
/// stack.
278334
pub unsafe fn stack_u64_mut(&mut self) -> &mut [u64] {
279-
assert!(
280-
self.slot().stack as usize % 8 == 0,
281-
"stack is 8-byte aligned"
282-
);
283-
assert!(
284-
self.slot().limits.stack_size % 8 == 0,
285-
"stack size is multiple of 8-bytes"
286-
);
287-
std::slice::from_raw_parts_mut(
288-
self.slot().stack as *mut u64,
289-
self.slot().limits.stack_size / 8,
290-
)
335+
let stack_start = self.stack_start();
336+
let stack_size = self.stack_size();
337+
338+
assert!(stack_start as usize % 8 == 0, "stack is 8-byte aligned");
339+
assert!(stack_size % 8 == 0, "stack size is multiple of 8-bytes");
340+
std::slice::from_raw_parts_mut(stack_start as *mut u64, stack_size / 8)
291341
}
292342

293343
/// Return the globals as a slice.

lucet-runtime/lucet-runtime-internals/src/context/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ pub struct Context {
115115
retvals_gp: [u64; 2],
116116
retval_fp: __m128,
117117
sigset: signal::SigSet,
118+
pub(crate) stop_addr: Option<u64>,
118119
}
119120

120121
impl Context {
@@ -126,6 +127,7 @@ impl Context {
126127
retvals_gp: [0; 2],
127128
retval_fp: unsafe { _mm_setzero_ps() },
128129
sigset: signal::SigSet::empty(),
130+
stop_addr: None,
129131
}
130132
}
131133
}

lucet-runtime/lucet-runtime-internals/src/instance.rs

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -949,63 +949,119 @@ impl Instance {
949949
Ok(())
950950
}
951951

952-
fn push(&mut self, value: u64) {
953-
let stack_offset = self.ctx.gpr.rsp as usize - self.alloc.slot().stack as usize;
952+
fn push(&mut self, value: u64) -> Result<(), ()> {
953+
let stack_offset = self.ctx.gpr.rsp as usize - self.alloc.stack_start() as usize;
954954
let stack_index = stack_offset / 8;
955955
assert!(stack_offset % 8 == 0);
956956

957957
let stack = unsafe { self.alloc.stack_u64_mut() };
958958

959959
// check for at least one free stack slot
960-
if stack.len() - stack_index >= 1 {
960+
if stack_index >= 1 {
961961
self.ctx.gpr.rsp -= 8;
962962
stack[stack_index - 1] = value;
963+
Ok(())
963964
} else {
964-
panic!("caused a guest stack overflow!");
965+
Err(())
965966
}
966967
}
967968

969+
fn with_redzone_stack<T, F: FnOnce(&mut Self) -> T>(&mut self, f: F) -> T {
970+
self.alloc.enable_stack_redzone();
971+
972+
let res = f(self);
973+
974+
self.alloc.disable_stack_redzone();
975+
976+
res
977+
}
978+
979+
// Force a guest to unwind the stack from the specified guest address
968980
fn force_unwind(&mut self) -> Result<(), Error> {
969-
#[unwind(allowed)]
970-
extern "C" fn initiate_unwind() {
971-
panic!(TerminationDetails::ForcedUnwind);
972-
}
981+
// if we should unwind by returning into the guest to cause a fault, do so with the redzone
982+
// available in case the guest was at or close to overflowing.
983+
self.with_redzone_stack(|inst| {
984+
#[unwind(allowed)]
985+
extern "C" fn initiate_unwind() {
986+
panic!(TerminationDetails::ForcedUnwind);
987+
}
973988

974-
// The logic for this conditional can be a bit unintuitive: we _require_ that the stack
975-
// is aligned to 8 bytes, but not 16 bytes, when pushing `initiate_unwind`.
976-
//
977-
// A diagram of the required layout may help:
978-
// `XXXXX0`: ------------------ <-- call frame start -- SysV ABI requires 16-byte alignment
979-
// `XXXXX8`: | return address |
980-
// `XXXX..`: | ..locals etc.. |
981-
// `XXXX..`: | ..as needed... |
982-
//
983-
// By the time we've gotten here, we have already pushed "return address", the address of
984-
// wherever in the guest we want to start unwinding. If it leaves the stack 16-byte
985-
// aligned, it's 8 bytes off from the diagram above, and we would have the call frame for
986-
// `initiate_unwind` in violation of the SysV ABI. Functionally, this means that
987-
// compiler-generated xmm accesses will fault due to being misaligned.
988-
//
989-
// So, instead, push a new return address to construct a new call frame at the right
990-
// offset. `unwind_stub` has CFA directives so the unwinder can connect from
991-
// `initiate_unwind` to guest/host frames to unwind. The unwinder, thankfully, has no
992-
// preferences about stack alignment of frames being unwound.
993-
//
994-
// extremely unsafe, doesn't handle any stack exhaustion edge cases yet
995-
if self.ctx.gpr.rsp % 16 == 0 {
996-
self.push(crate::context::unwind_stub as u64);
997-
}
989+
let guest_addr = inst
990+
.ctx
991+
.stop_addr
992+
.expect("guest that stopped in guest code has an address it stopped at");
993+
994+
// set up the faulting instruction pointer as the return address for `initiate_unwind`;
995+
// extremely unsafe, doesn't handle any edge cases yet
996+
//
997+
// TODO(Andy) if the last address is obtained through the signal handler, for a signal
998+
// received exactly when we have just executed a `call` to a guest function, we
999+
// actually want to not push it (or push it +1?) lest we try to unwind with a return
1000+
// address == start of function, where the system unwinder will unwind for the function
1001+
// at address-1, (probably) fail to find the function, and `abort()`.
1002+
//
1003+
// if `rip` == the start of some guest function, we can probably just discard it and
1004+
// use the return address instead.
1005+
inst.push(guest_addr as u64)
1006+
.expect("stack has available space");
1007+
1008+
// The logic for this conditional can be a bit unintuitive: we _require_ that the stack
1009+
// is aligned to 8 bytes, but not 16 bytes, when pushing `initiate_unwind`.
1010+
//
1011+
// A diagram of the required layout may help:
1012+
// `XXXXX0`: ------------------ <-- call frame start -- SysV ABI requires 16-byte alignment
1013+
// `XXXXX8`: | return address |
1014+
// `XXXX..`: | ..locals etc.. |
1015+
// `XXXX..`: | ..as needed... |
1016+
//
1017+
// Now ensure we _have_ an ABI-conformant call fame like above, by handling the case that
1018+
// could lead to an unaligned stack - the guest stack pointer currently being unaligned.
1019+
// Among other errors, a misaligned stack will result in compiler-generated xmm accesses to
1020+
// fault.
1021+
//
1022+
// Eg, we would have a stack like:
1023+
// `XXXXX8`: ------------------ <-- guest stack end, call frame start
1024+
// `XXXXX0`: | unwind_stub |
1025+
// `XXXX..`: | ..locals etc.. |
1026+
// `XXXX..`: | ..as needed... |
1027+
//
1028+
// So, instead, push a new return address to construct a new call frame at the right
1029+
// offset. `unwind_stub` has CFA directives so the unwinder can connect from
1030+
// `initiate_unwind` to guest/host frames to unwind. The unwinder, thankfully, has no
1031+
// preferences about alignment of frames being unwound.
1032+
//
1033+
// And we end up with a guest stack like this:
1034+
// `XXXXX8`: ------------------ <-- guest stack end
1035+
// `XXXXX0`: | guest ret addr | <-- guest return address to unwind through
1036+
// `XXXXX0`: ------------------ <-- call frame start -- SysV ABI requires 16-byte alignment
1037+
// `XXXXX8`: | unwind_stub |
1038+
// `XXXX..`: | ..locals etc.. |
1039+
// `XXXX..`: | ..as needed... |
1040+
if inst.ctx.gpr.rsp % 16 == 0 {
1041+
// extremely unsafe, doesn't handle any stack exhaustion edge cases yet
1042+
inst.push(crate::context::unwind_stub as u64)
1043+
.expect("stack has available space");
1044+
}
9981045

999-
assert!(self.ctx.gpr.rsp % 16 == 8);
1000-
self.push(initiate_unwind as u64);
1046+
assert!(inst.ctx.gpr.rsp % 16 == 8);
1047+
// extremely unsafe, doesn't handle any stack exhaustion edge cases yet
1048+
inst.push(initiate_unwind as u64)
1049+
.expect("stack has available space");
10011050

1002-
match self.swap_and_return() {
1003-
Ok(_) => panic!("forced unwinding shouldn't return normally"),
1004-
Err(Error::RuntimeTerminated(TerminationDetails::ForcedUnwind)) => (),
1005-
Err(e) => panic!("unexpected error: {}", e),
1006-
}
1051+
inst.state = State::Ready;
10071052

1008-
Ok(())
1053+
match inst.swap_and_return() {
1054+
Ok(_) => panic!("forced unwinding shouldn't return normally"),
1055+
Err(Error::RuntimeTerminated(TerminationDetails::ForcedUnwind)) => (),
1056+
Err(e) => panic!("unexpected error: {}", e),
1057+
}
1058+
1059+
// we've unwound the stack, so we know there are no longer any host frames.
1060+
inst.hostcall_count = 0;
1061+
inst.ctx.stop_addr = None;
1062+
1063+
Ok(())
1064+
})
10091065
}
10101066
}
10111067

lucet-runtime/lucet-runtime-internals/src/instance/signals.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -226,24 +226,7 @@ extern "C" fn handle_signal(signum: c_int, siginfo_ptr: *mut siginfo_t, ucontext
226226
// `Context::swap()` here because then we'd swap back to the signal handler instead of
227227
// the point in the guest that caused the fault
228228
ctx.save_to_context(&mut inst.ctx);
229-
230-
// set up the faulting instruction pointer as the return address for `initiate_unwind`;
231-
// extremely unsafe, doesn't handle any edge cases yet
232-
//
233-
// TODO(Andy) can we avoid pushing onto the guest stack until knowing we want to force
234-
// an unwind? maybe a "last address" field on ctx. This can be populated on context
235-
// swap out (return address of lucet_context_swap) as well as here in the signal
236-
// handler.
237-
//
238-
// TODO(Andy) if the last address is obtained through the signal handler, for a signal
239-
// received exactly when we have just executed a `call` to a guest function, we
240-
// actually want to not push it (or push it +1?) lest we try to unwind with a return
241-
// address == start of function, where the system unwinder will unwind for the function
242-
// at address-1, (probably) fail to find the function, and `abort()`.
243-
//
244-
// if `rip` == the start of some guest function, we can probably just discard it and
245-
// use the return address instead.
246-
inst.push(rip as u64);
229+
inst.ctx.stop_addr = Some(rip as u64);
247230
}
248231
switch_to_host
249232
});

lucet-runtime/lucet-runtime-internals/src/region/mmap.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,28 @@ impl RegionInternal for MmapRegion {
246246
fn as_dyn_internal(&self) -> &dyn RegionInternal {
247247
self
248248
}
249+
250+
fn enable_stack_redzone(&self, slot: &Slot) {
251+
unsafe {
252+
mprotect(
253+
slot.stack_redzone_start(),
254+
host_page_size(),
255+
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
256+
)
257+
.expect("can set permissions on stack redzone page")
258+
}
259+
}
260+
261+
fn disable_stack_redzone(&self, slot: &Slot) {
262+
unsafe {
263+
mprotect(
264+
slot.stack_redzone_start(),
265+
host_page_size(),
266+
ProtFlags::PROT_NONE,
267+
)
268+
.expect("can set permissions on stack redzone page")
269+
}
270+
}
249271
}
250272

251273
impl Drop for MmapRegion {
@@ -329,6 +351,7 @@ impl MmapRegion {
329351
sigstack: sigstack as *mut c_void,
330352
limits: region.limits.clone(),
331353
region: Arc::downgrade(region) as Weak<dyn RegionInternal>,
354+
redzone_stack_enabled: false,
332355
})
333356
}
334357

lucet-runtime/lucet-runtime-internals/src/region/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ pub trait RegionInternal: Send + Sync {
5252
fn reset_heap(&self, alloc: &mut Alloc, module: &dyn Module) -> Result<(), Error>;
5353

5454
fn as_dyn_internal(&self) -> &dyn RegionInternal;
55+
56+
fn enable_stack_redzone(&self, slot: &Slot);
57+
58+
fn disable_stack_redzone(&self, slot: &Slot);
5559
}
5660

5761
/// A trait for regions that are created with a fixed capacity and limits.

lucet-runtime/lucet-runtime-tests/guests/host/bindings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"env": {
3-
"fault_unwind": "hostcall_fault_unwind",
3+
"bad_access_unwind": "hostcall_bad_access_unwind",
4+
"stack_overflow_unwind": "hostcall_stack_overflow_unwind",
45
"hostcall_test_func_hello": "hostcall_test_func_hello",
56
"hostcall_test_func_hostcall_error": "hostcall_test_func_hostcall_error",
67
"hostcall_test_func_hostcall_error_unwind": "hostcall_test_func_hostcall_error_unwind",

0 commit comments

Comments
 (0)