diff --git a/Cargo.lock b/Cargo.lock index 6907d9a64..cdd4f30c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,7 +6,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543" dependencies = [ - "gimli 0.21.0", + "gimli", ] [[package]] @@ -306,21 +306,21 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.64.0" +version = "0.65.0" dependencies = [ "byteorder", "cranelift-bforest", "cranelift-codegen-meta", "cranelift-codegen-shared", "cranelift-entity", - "gimli 0.20.0", + "gimli", "log", "regalloc", "smallvec", @@ -330,7 +330,7 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cranelift-codegen-shared", "cranelift-entity", @@ -338,15 +338,15 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.64.0" +version = "0.65.0" [[package]] name = "cranelift-entity" -version = "0.64.0" +version = "0.65.0" [[package]] name = "cranelift-frontend" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cranelift-codegen", "log", @@ -356,7 +356,7 @@ dependencies = [ [[package]] name = "cranelift-module" -version = "0.64.0" +version = "0.65.0" dependencies = [ "anyhow", "cranelift-codegen", @@ -367,7 +367,7 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cranelift-codegen", "raw-cpuid 7.0.3", @@ -376,17 +376,18 @@ dependencies = [ [[package]] name = "cranelift-object" -version = "0.64.0" +version = "0.65.0" dependencies = [ + "anyhow", "cranelift-codegen", "cranelift-module", - "object 0.18.0", + "object 0.19.0", "target-lexicon", ] [[package]] name = "cranelift-wasm" -version = "0.64.0" +version = "0.65.0" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -680,20 +681,13 @@ dependencies = [ [[package]] name = "gimli" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dd6190aad0f05ddbbf3245c54ed14ca4aa6dd32f22312b70d8f168c3e3e633" +checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" dependencies = [ - "byteorder", "indexmap", ] -[[package]] -name = "gimli" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" - [[package]] name = "glob" version = "0.2.11" @@ -956,6 +950,7 @@ name = "lucet-runtime-internals" version = "0.7.0-dev" dependencies = [ "anyhow", + "backtrace", "bincode", "bitflags 1.2.1", "byteorder", @@ -1157,6 +1152,7 @@ dependencies = [ "cranelift-object", "cranelift-wasm", "env_logger", + "gimli", "human-size", "log", "lucet-module", @@ -1164,7 +1160,7 @@ dependencies = [ "lucet-wiggle-generate", "memoffset", "minisign", - "object 0.18.0", + "object 0.19.0", "raw-cpuid 6.1.0", "serde", "serde_json", @@ -1307,9 +1303,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5666bbb90bc4d1e5bdcb26c0afda1822d25928341e9384ab187a9b37ab69e36" dependencies = [ - "crc32fast", "flate2", - "indexmap", "target-lexicon", "wasmparser 0.51.4", ] @@ -1319,6 +1313,10 @@ name = "object" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2" +dependencies = [ + "crc32fast", + "indexmap", +] [[package]] name = "oorandom" @@ -1702,9 +1700,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" [[package]] name = "regalloc" -version = "0.0.25" +version = "0.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca5b48c9db66c5ba084e4660b4c0cfe8b551a96074bc04b7c11de86ad0bf1f9" +checksum = "7c03092d79e0fd610932d89ed53895a38c0dd3bcd317a0046e69940de32f1d95" dependencies = [ "log", "rustc-hash", @@ -2284,7 +2282,7 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi-common" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "cfg-if", @@ -2395,7 +2393,7 @@ dependencies = [ [[package]] name = "wig" -version = "0.17.0" +version = "0.18.0" dependencies = [ "heck", "proc-macro2 1.0.13", @@ -2405,7 +2403,7 @@ dependencies = [ [[package]] name = "wiggle" -version = "0.17.0" +version = "0.18.0" dependencies = [ "thiserror", "tracing", @@ -2415,7 +2413,7 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "heck", @@ -2427,7 +2425,7 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "0.17.0" +version = "0.18.0" dependencies = [ "quote 1.0.6", "syn 1.0.22", @@ -2437,7 +2435,7 @@ dependencies = [ [[package]] name = "wiggle-test" -version = "0.17.0" +version = "0.18.0" dependencies = [ "proptest", "wiggle", @@ -2488,7 +2486,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winx" -version = "0.17.0" +version = "0.18.0" dependencies = [ "bitflags 1.2.1", "cvt", @@ -2513,7 +2511,7 @@ checksum = "da90eac47bf1d7871b75004b9b631d107df15f37669383b23f0b5297bc7516b6" [[package]] name = "yanix" -version = "0.17.0" +version = "0.18.0" dependencies = [ "bitflags 1.2.1", "cfg-if", diff --git a/benchmarks/lucet-benchmarks/src/lib.rs b/benchmarks/lucet-benchmarks/src/lib.rs index 989b582af..28a866483 100644 --- a/benchmarks/lucet-benchmarks/src/lib.rs +++ b/benchmarks/lucet-benchmarks/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(unwind_attributes)] + mod compile; mod context; mod modules; diff --git a/helpers/indent.sh b/helpers/indent.sh index 4fd6d1658..90e0f35dd 100755 --- a/helpers/indent.sh +++ b/helpers/indent.sh @@ -10,7 +10,7 @@ cleanup() { } trap cleanup 1 2 3 6 15 -RUSTFMT_VERSION=1.4.12-stable +RUSTFMT_VERSION=1.4.15-nightly if ! rustfmt --version | grep -q "rustfmt $RUSTFMT_VERSION"; then echo "indent requires rustfmt $RUSTFMT_VERSION" diff --git a/lucet-module/Cargo.toml b/lucet-module/Cargo.toml index 4c6c719f2..33371a06d 100644 --- a/lucet-module/Cargo.toml +++ b/lucet-module/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" [dependencies] anyhow = "1.0" -cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.64.0" } +cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.65.0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" bincode = "1.1.4" diff --git a/lucet-runtime/lucet-runtime-internals/Cargo.toml b/lucet-runtime/lucet-runtime-internals/Cargo.toml index 34e92478c..7a8d1655d 100644 --- a/lucet-runtime/lucet-runtime-internals/Cargo.toml +++ b/lucet-runtime/lucet-runtime-internals/Cargo.toml @@ -14,6 +14,7 @@ lucet-module = { path = "../../lucet-module", version = "=0.7.0-dev" } lucet-runtime-macros = { path = "../lucet-runtime-macros", version = "=0.7.0-dev" } anyhow = "1.0" +backtrace = "0.3" bitflags = "1.0" bincode = "1.1.4" byteorder = "1.3" diff --git a/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs b/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs index eafbf2f6c..f08612cf0 100644 --- a/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs +++ b/lucet-runtime/lucet-runtime-internals/src/alloc/mod.rs @@ -58,6 +58,8 @@ pub struct Slot { pub limits: Limits, pub region: Weak, + + pub(crate) redzone_stack_enabled: bool, } // raw pointers require unsafe impl @@ -68,6 +70,14 @@ impl Slot { pub fn stack_top(&self) -> *mut c_void { (self.stack as usize + self.limits.stack_size) as *mut c_void } + + pub fn stack_redzone_start(&self) -> *mut c_void { + (self.stack as usize - self.stack_redzone_size()) as *mut c_void + } + + pub fn stack_redzone_size(&self) -> usize { + host_page_size() + } } /// The strategy by which a `Region` selects an allocation to back an `Instance`. @@ -167,6 +177,24 @@ impl AddrLocation { } impl Alloc { + pub(crate) fn enable_stack_redzone(&mut self) { + let slot = self + .slot + .as_mut() + .expect("alloc has a Slot when toggling stack redzone"); + slot.redzone_stack_enabled = true; + self.region.enable_stack_redzone(slot) + } + + pub(crate) fn disable_stack_redzone(&mut self) { + let slot = self + .slot + .as_mut() + .expect("alloc has a Slot when toggling stack redzone"); + slot.redzone_stack_enabled = false; + self.region.disable_stack_redzone(slot) + } + /// Where in an `Alloc` does a particular address fall? pub fn addr_location(&self, addr: *const c_void) -> AddrLocation { let addr = addr as usize; @@ -351,13 +379,41 @@ impl Alloc { std::slice::from_raw_parts_mut(self.slot().heap as *mut u64, self.heap_accessible_size / 8) } + pub(crate) fn stack_start(&self) -> *mut u8 { + let mut stack_start = self.slot().stack as usize; + + if self + .slot + .as_ref() + .expect("alloc has a slot when we want to access its stack") + .redzone_stack_enabled + { + stack_start -= host_page_size(); + } + + stack_start as *mut u8 + } + + pub(crate) fn stack_size(&self) -> usize { + let mut stack_size = self.slot().limits.stack_size; + if self + .slot + .as_ref() + .expect("alloc has a slot when we want to access its stack") + .redzone_stack_enabled + { + stack_size += host_page_size(); + } + stack_size + } + /// Return the stack as a mutable byte slice. /// /// Since the stack grows down, `alloc.stack_mut()[0]` is the top of the stack, and /// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last byte at the bottom of the /// stack. pub unsafe fn stack_mut(&mut self) -> &mut [u8] { - std::slice::from_raw_parts_mut(self.slot().stack as *mut u8, self.slot().limits.stack_size) + std::slice::from_raw_parts_mut(self.stack_start(), self.stack_size()) } /// Return the stack as a mutable slice of 64-bit words. @@ -366,18 +422,12 @@ impl Alloc { /// `alloc.stack_mut()[alloc.limits.stack_size - 1]` is the last word at the bottom of the /// stack. pub unsafe fn stack_u64_mut(&mut self) -> &mut [u64] { - assert!( - self.slot().stack as usize % 8 == 0, - "stack is 8-byte aligned" - ); - assert!( - self.slot().limits.stack_size % 8 == 0, - "stack size is multiple of 8-bytes" - ); - std::slice::from_raw_parts_mut( - self.slot().stack as *mut u64, - self.slot().limits.stack_size / 8, - ) + let stack_start = self.stack_start(); + let stack_size = self.stack_size(); + + assert!(stack_start as usize % 8 == 0, "stack is 8-byte aligned"); + assert!(stack_size % 8 == 0, "stack size is multiple of 8-bytes"); + std::slice::from_raw_parts_mut(stack_start as *mut u64, stack_size / 8) } /// Return the globals as a slice. diff --git a/lucet-runtime/lucet-runtime-internals/src/c_api.rs b/lucet-runtime/lucet-runtime-internals/src/c_api.rs index 393cf4cdf..e263071c2 100644 --- a/lucet-runtime/lucet-runtime-internals/src/c_api.rs +++ b/lucet-runtime/lucet-runtime-internals/src/c_api.rs @@ -311,6 +311,10 @@ pub mod lucet_result { .map(|CTerminationDetails { details }| *details) .unwrap_or(ptr::null_mut()), }, + TerminationDetails::ForcedUnwind => lucet_terminated { + reason: lucet_terminated_reason::ForcedUnwind, + provided: std::ptr::null_mut(), + }, TerminationDetails::Remote => lucet_terminated { reason: lucet_terminated_reason::Remote, provided: std::ptr::null_mut(), @@ -368,6 +372,7 @@ pub mod lucet_result { YieldTypeMismatch, BorrowError, Provided, + ForcedUnwind, Remote, } diff --git a/lucet-runtime/lucet-runtime-internals/src/context/context_asm.S b/lucet-runtime/lucet-runtime-internals/src/context/context_asm.S index f313ddea9..89452e40c 100644 --- a/lucet-runtime/lucet-runtime-internals/src/context/context_asm.S +++ b/lucet-runtime/lucet-runtime-internals/src/context/context_asm.S @@ -55,13 +55,122 @@ _lucet_context_bootstrap: #endif .text +.globl unwind_stub +.align 16 +.cfi_startproc + // load bearing nop so we can just put `unwind_stub` in places and have the unwinding mechanism understand we mean we are _in_ this function + nop +unwind_stub: +_unwind_stub: + ret +.cfi_endproc +#ifdef __ELF__ +.size unwind_stub,.-unwind_stub +#endif + +.text +.globl libunwind_backstop_shim +.align 16 +// unsure if `simple` is necessary here. GAS docs suggest this may omit some initial CFI instructions, and i want this to be as barren as possible for now. +.cfi_startproc simple + // .cfi_personality 0,win + + // TODO: generate these CFI instructions, perhaps using gimli + + // The idea with the call frame information here is to make any unwinder (libgcc, gdb, + // libunwind, etc) think that the previous frame is the call to `Context::swap()` that initially swapped + // us from host context into guest context. This means indicating that the values of + // callee-saved registers may be found within the saved host context, and that the canonical + // frame address is in the host stack. + + // The values we want to use are behind two dereferences (address of the parent context, parent + // context), so the normal `.cfi_*` assembler directives are not sufficient to specify their + // location. Instead, we have to use `.cfi_escape` so that we can write DWARF expressions that + // locate the values or their addresses. See the DWARF spec for more info. + + // Start by providing the canonical frame address. Even though unwinding runtimes _should_ be + // able to figure this out based on the saved rsp value, things go wrong if this is missing. + + .cfi_escape 0x0f, /* DW_CFA_def_cfa_expression */ \ + 9, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x08, /* DW_OP_plus_uconst (add 8 to the base of context to point to saved rsp) */ \ + 0x06, /* deref */ \ + 0x23, 0x08 /* add 8 to pop the return address from the call to lucet_context_swap */ + + // Now provide the saved rsp value. Note that unlike the other callee-saved register + // expressions, this is a `val-expression` so that we can increment the final value to account for + // the extra `lucet_context_swap` call frame we want to skip over + + .cfi_escape 0x16, 0x07, /* DW_CFA_val_expression(7=rsp) */ \ + 9, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x08, /* DW_OP_plus_uconst (add 8 to the base of context to point to saved rsp) */ \ + 0x06, /* deref */ \ + 0x23, 0x08 /* add 8 to pop the return address from the call to lucet_context_swap */ + + // The remaining expressions are all very similar; they just return the address that points to + // the corresponding field on the `Context` struct + + .cfi_escape 0x10, 0x03, /* DW_CFA_expression(3=rbx) */ \ + 4, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06 /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + /* rbx is at offset 0 */ + + .cfi_escape 0x10, 0x06, /* DW_CFA_expression(6=rbp) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x10 /* DW_OP_plus_uconst (add 16 to the base of context to point to saved rbp) */ + + .cfi_escape 0x10, 0x05, /* DW_CFA_expression(5=rdi) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x18 /* DW_OP_plus_uconst (add 24 to the base of context to point to saved rdi) */ + + .cfi_escape 0x10, 0x0c, /* DW_CFA_expression(12=r12) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x20 /* DW_OP_plus_uconst (add 32 to the base of context to point to saved r12) */ + + .cfi_escape 0x10, 0x0d, /* DW_CFA_expression(13=r13) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x28 /* DW_OP_plus_uconst (add 40 to the base of context to point to saved r13) */ + + .cfi_escape 0x10, 0x0e, /* DW_CFA_expression(14=r14) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x30 /* DW_OP_plus_uconst (add 48 to the base of context to point to saved r14) */ + + .cfi_escape 0x10, 0x0f, /* DW_CFA_expression(15=r15) */ \ + 6, /* uleb128 length of expression bytes */ \ + 0x76, 0xf0, 0x01, /* DW_OP_breg5 (put rbp + 240 on the stack; should be a pointer to the parent context's address) */ \ + 0x06, /* DW_OP_deref should then leave the parent context pointer on the stack */ \ + 0x23, 0x38 /* DW_OP_plus_uconst (add 56 to the base of context to point to saved r15) */ + + .cfi_offset 16, -8 /* return address value is stored at is CFA -8 */ + + // this nop is load-bearing. We set up the backstop with a cfi_personality function so that stack unwinding through + // guest code ends at a handler which can recover and switch back to the host. + // However: libunwind doesn't treat the start of `lucet_context_backstop` as `lucet_context_backstop` but as the end + // of `lucet_context_bootstrap`. so to get the unwinder to reach information about this function, start the unwind + // region slightly before the real address we use, and place an address after the start, unambiguously referring to + // the backstop function, on the stack for guests (and libunwind!) to eventually reach. + nop .globl lucet_context_backstop #ifdef __ELF__ .type lucet_context_backstop,@function #else .globl _lucet_context_backstop #endif -.align 16 lucet_context_backstop: _lucet_context_backstop: // Note that `rbp` here really has no relation to any stack! @@ -94,6 +203,7 @@ no_backstop_callback: #else jmp lucet_context_swap #endif +.cfi_endproc #ifdef __ELF__ .size lucet_context_backstop,.-lucet_context_backstop #endif diff --git a/lucet-runtime/lucet-runtime-internals/src/context/mod.rs b/lucet-runtime/lucet-runtime-internals/src/context/mod.rs index a6f3b13b3..6afcae529 100644 --- a/lucet-runtime/lucet-runtime-internals/src/context/mod.rs +++ b/lucet-runtime/lucet-runtime-internals/src/context/mod.rs @@ -25,15 +25,15 @@ use thiserror::Error; /// `u64`, this should be fine? #[repr(C)] pub(crate) struct GpRegs { - pub(crate) rbx: u64, - pub(crate) rsp: u64, - rbp: u64, - pub(crate) rdi: u64, - r12: u64, - r13: u64, - r14: u64, - r15: u64, - pub(crate) rsi: u64, + pub rbx: u64, + pub rsp: u64, + pub rbp: u64, + pub rdi: u64, + pub r12: u64, + pub r13: u64, + pub r14: u64, + pub r15: u64, + pub rsi: u64, } impl GpRegs { @@ -65,15 +65,15 @@ impl GpRegs { /// . Since the members are all /// `__m128`, this should be fine? #[repr(C)] -struct FpRegs { - xmm0: __m128, - xmm1: __m128, - xmm2: __m128, - xmm3: __m128, - xmm4: __m128, - xmm5: __m128, - xmm6: __m128, - xmm7: __m128, +pub(crate) struct FpRegs { + pub xmm0: __m128, + pub xmm1: __m128, + pub xmm2: __m128, + pub xmm3: __m128, + pub xmm4: __m128, + pub xmm5: __m128, + pub xmm6: __m128, + pub xmm7: __m128, } impl FpRegs { @@ -115,7 +115,7 @@ impl FpRegs { #[repr(C, align(64))] pub struct Context { pub(crate) gpr: GpRegs, - fpr: FpRegs, + pub(crate) fpr: FpRegs, retvals_gp: [u64; 2], retval_fp: __m128, parent_ctx: *mut Context, @@ -729,4 +729,8 @@ extern "C" { /// For more information, see `Instance::swap_and_return`, `Instance::with_activation_routine`, /// `enter_guest_region`, and `lucet_context_activate`'s assembly implementation. pub(crate) fn lucet_context_activate(); + + /// Just a stub to have a single address we can unwind through, for stack alignment when we + /// need to add a new call frame to an existing guest + pub(crate) fn unwind_stub(); } diff --git a/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs b/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs index 75d923d75..da7284763 100644 --- a/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs +++ b/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs @@ -21,6 +21,10 @@ /// // body /// } /// ``` +/// +/// **Note:** This macro currently uses the unstable `#![feature(unwind_attributes)]`, which must be +/// enabled in any crate where the macro is used. In the long term, we hope to move back to stable +/// once [unwinding across FFI](https://github.com/rust-lang/rfcs/pull/2753) is defined. #[macro_export] #[deprecated(since = "0.5.0", note = "Use the #[lucet_hostcall] attribute instead")] macro_rules! lucet_hostcalls { @@ -40,11 +44,39 @@ macro_rules! lucet_hostcalls { #[allow(unused_unsafe)] #[$crate::lucet_hostcall] $(#[$attr])* + #[unwind(allowed)] pub unsafe extern "C" fn $name( $vmctx: &lucet_runtime::vmctx::Vmctx, $( $arg: $arg_ty ),* ) -> $ret_ty { - $($body)* + #[inline(always)] + unsafe fn hostcall_impl( + $vmctx: &mut $crate::vmctx::Vmctx, + $( $arg : $arg_ty ),* + ) -> $ret_ty { + $($body)* + } + // let res = std::panic::catch_unwind(move || { + #[allow(unused_imports)] + use lucet_runtime_internals::vmctx::VmctxInternal; + let res = $crate::vmctx::Vmctx::from_raw(vmctx_raw).instance_mut().in_hostcall(|| { + hostcall_impl(&mut $crate::vmctx::Vmctx::from_raw(vmctx_raw), $( $arg ),*) + }); + res + // }); + // match res { + // Ok(res) => res, + // Err(e) => { + // if let Some(details) = e.downcast_ref::<$crate::instance::TerminationDetails>() { + // let mut vmctx = $crate::vmctx::Vmctx::from_raw(vmctx_raw); + // vmctx.terminate_no_unwind(details.clone()); + // } else { + // std::panic::resume_unwind(e); + // } + // } + // } + // + // res } )* } diff --git a/lucet-runtime/lucet-runtime-internals/src/instance.rs b/lucet-runtime/lucet-runtime-internals/src/instance.rs index f00342ef3..875ef8d46 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance.rs @@ -18,6 +18,7 @@ use crate::region::RegionInternal; use crate::sysdeps::HOST_PAGE_SIZE_EXPECTED; use crate::val::{UntypedRetVal, Val}; use crate::WASM_PAGE_SIZE; +use backtrace::Backtrace; use libc::{c_void, pthread_self, siginfo_t, uintptr_t}; use lucet_module::InstanceRuntimeData; use memoffset::offset_of; @@ -26,6 +27,7 @@ use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut, UnsafeCell}; use std::marker::PhantomData; use std::mem; use std::ops::{Deref, DerefMut}; +use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; use std::ptr::{self, NonNull}; use std::sync::Arc; @@ -44,7 +46,7 @@ thread_local! { /// the swap. Meanwhile, the signal handler can run at any point during the guest function, and /// so it also must be able to immutably borrow the host context if it needs to swap back. The /// runtime borrowing constraints for a `RefCell` are therefore too strict for this variable. - pub(crate) static HOST_CTX: UnsafeCell = UnsafeCell::new(Context::new()); + pub static HOST_CTX: UnsafeCell = UnsafeCell::new(Context::new()); /// The currently-running `Instance`, if one exists. pub(crate) static CURRENT_INSTANCE: RefCell>> = RefCell::new(None); @@ -219,7 +221,7 @@ pub struct Instance { module: Arc, /// The `Context` in which the guest program runs - pub(crate) ctx: Context, + pub ctx: Context, /// Instance state and error information pub(crate) state: State, @@ -232,7 +234,7 @@ pub struct Instance { pub lock_testpoints: Arc, /// The memory allocated for this instance - alloc: Alloc, + pub alloc: Alloc, /// Handler run for signals that do not arise from a known WebAssembly trap, or that involve /// memory outside of the current instance. @@ -264,6 +266,10 @@ pub struct Instance { /// The value passed back to the guest when resuming a yielded instance. pub(crate) resumed_val: Option>, + hostcall_count: u64, + + pub(crate) pending_termination: Option, + /// `_padding` must be the last member of the structure. /// This marks where the padding starts to make the structure exactly 4096 bytes long. /// It is also used to compute the size of the structure up to that point, i.e. without padding. @@ -279,6 +285,10 @@ pub struct Instance { /// this Instance exists in, *while* the Instance destructor is executing. impl Drop for Instance { fn drop(&mut self) { + // Initiate a forced unwind if any hostcall frames remain on the stack + if self.hostcall_count > 0 { + self.force_unwind().expect("forced unwinding succeeds"); + } // Reset magic to indicate this instance // is no longer valid self.magic = 0; @@ -592,6 +602,9 @@ impl Instance { /// /// [run_start]: struct.Instance.html#method.run pub fn reset(&mut self) -> Result<(), Error> { + if self.hostcall_count > 0 { + self.force_unwind()?; + } self.alloc.reset_heap(self.module.as_ref())?; let globals = unsafe { self.alloc.globals_mut() }; let mod_globals = self.module.globals(); @@ -612,6 +625,9 @@ impl Instance { } else { self.state = State::Ready; } + self.resumed_val = None; + self.hostcall_count = 0; + self.pending_termination = None; #[cfg(feature = "concurrent_testpoints")] { @@ -839,6 +855,19 @@ impl Instance { res } + pub fn in_hostcall R + UnwindSafe, R>(&mut self, f: F) -> R { + self.hostcall_count += 1; + let res = match catch_unwind(f) { + Ok(res) => res, + Err(e) => { + self.hostcall_count -= 1; + resume_unwind(e) + } + }; + self.hostcall_count -= 1; + res + } + #[inline] pub fn get_instruction_count(&self) -> Option { if self.module.is_instruction_count_instrumented() { @@ -883,6 +912,8 @@ impl Instance { ensure_sigstack_installed: true, entrypoint: None, resumed_val: None, + hostcall_count: 0, + pending_termination: None, _padding: (), }; inst.set_globals_ptr(globals_ptr); @@ -976,6 +1007,10 @@ impl Instance { let mut args_with_vmctx = vec![Val::from(self.alloc.slot().heap)]; args_with_vmctx.extend_from_slice(args); + // when preparing a new context for the guest to run, we must have cleaned up any existing + // host call frames on the stack. + assert_eq!(self.hostcall_count, 0); + let self_ptr = self as *mut _; Context::init_with_callback( unsafe { self.alloc.stack_u64_mut() }, @@ -1122,6 +1157,7 @@ impl Instance { mut details, siginfo, context, + full_backtrace, } => { // Sandbox is no longer runnable. It's unsafe to determine all error details in the signal // handler, so we fill in extra details here. @@ -1132,11 +1168,15 @@ impl Instance { .module .addr_details(details.rip_addr as *const c_void)?; + details.backtrace = Some(self.module.resolve_and_trim(&full_backtrace)); + // dbg!(&details.backtrace); + // fill the state back in with the updated details in case fatal handlers need it self.state = State::Faulted { details: details.clone(), siginfo, context, + full_backtrace, }; if details.fatal { @@ -1195,6 +1235,137 @@ impl Instance { res } + + fn push(&mut self, value: u64) -> Result<(), ()> { + let stack_offset = self.ctx.gpr.rsp as usize - self.alloc.stack_start() as usize; + let stack_index = stack_offset / 8; + assert!(stack_offset % 8 == 0); + + let stack = unsafe { self.alloc.stack_u64_mut() }; + + // check for at least one free stack slot + if stack_index >= 1 { + self.ctx.gpr.rsp -= 8; + stack[stack_index - 1] = value; + Ok(()) + } else { + Err(()) + } + } + + fn with_redzone_stack T>(&mut self, f: F) -> T { + self.alloc.enable_stack_redzone(); + + let res = f(self); + + self.alloc.disable_stack_redzone(); + + res + } + + // Force a guest to unwind the stack from the specified guest address + fn force_unwind(&mut self) -> Result<(), Error> { + match &mut self.state { + State::Yielded { .. } => { + self.pending_termination = Some(TerminationDetails::ForcedUnwind); + match self.with_redzone_stack(|inst| inst.swap_and_return()) { + Err(Error::RuntimeTerminated(TerminationDetails::ForcedUnwind)) => Ok(()), + Err(e) => lucet_bail!( + "resume with forced unwind returned with an unexpected error: {}", + e + ), + Ok(_) => lucet_bail!("resume with forced unwind returned normally"), + } + } + State::Faulted { + context, details, .. + } => { + #[unwind(allowed)] + extern "C" fn initiate_unwind() { + panic!(TerminationDetails::ForcedUnwind); + } + + // set up the guest context as it was when the fault was raised + context.as_ptr().save_to_context(&mut self.ctx); + + // get the instruction pointer when the fault was raised + let guest_addr = details.rip_addr; + + // if we should unwind by returning into the guest to cause a fault, do so with the redzone + // available in case the guest was at or close to overflowing. + self.with_redzone_stack(|inst| { + // set up the faulting instruction pointer as the return address for `initiate_unwind`; + // extremely unsafe, doesn't handle any edge cases yet + // + // TODO(Andy) if the last address is obtained through the signal handler, for a signal + // received exactly when we have just executed a `call` to a guest function, we + // actually want to not push it (or push it +1?) lest we try to unwind with a return + // address == start of function, where the system unwinder will unwind for the function + // at address-1, (probably) fail to find the function, and `abort()`. + // + // if `rip` == the start of some guest function, we can probably just discard it and + // use the return address instead. + inst.push(guest_addr as u64) + .expect("stack has available space"); + + // The logic for this conditional can be a bit unintuitive: we _require_ that the stack + // is aligned to 8 bytes, but not 16 bytes, when pushing `initiate_unwind`. + // + // A diagram of the required layout may help: + // `XXXXX0`: ------------------ <-- call frame start -- SysV ABI requires 16-byte alignment + // `XXXXX8`: | return address | + // `XXXX..`: | ..locals etc.. | + // `XXXX..`: | ..as needed... | + // + // Now ensure we _have_ an ABI-conformant call fame like above, by handling the case that + // could lead to an unaligned stack - the guest stack pointer currently being unaligned. + // Among other errors, a misaligned stack will result in compiler-generated xmm accesses to + // fault. + // + // Eg, we would have a stack like: + // `XXXXX8`: ------------------ <-- guest stack end, call frame start + // `XXXXX0`: | unwind_stub | + // `XXXX..`: | ..locals etc.. | + // `XXXX..`: | ..as needed... | + // + // So, instead, push a new return address to construct a new call frame at the right + // offset. `unwind_stub` has CFA directives so the unwinder can connect from + // `initiate_unwind` to guest/host frames to unwind. The unwinder, thankfully, has no + // preferences about alignment of frames being unwound. + // + // And we end up with a guest stack like this: + // `XXXXX8`: ------------------ <-- guest stack end + // `XXXXX0`: | guest ret addr | <-- guest return address to unwind through + // `XXXXX0`: ------------------ <-- call frame start -- SysV ABI requires 16-byte alignment + // `XXXXX8`: | unwind_stub | + // `XXXX..`: | ..locals etc.. | + // `XXXX..`: | ..as needed... | + if inst.ctx.gpr.rsp % 16 == 0 { + // extremely unsafe, doesn't handle any stack exhaustion edge cases yet + inst.push(crate::context::unwind_stub as u64) + .expect("stack has available space"); + } + + assert!(inst.ctx.gpr.rsp % 16 == 8); + // extremely unsafe, doesn't handle any stack exhaustion edge cases yet + inst.push(initiate_unwind as u64) + .expect("stack has available space"); + + match inst.swap_and_return() { + Ok(_) => panic!("forced unwinding shouldn't return normally"), + Err(Error::RuntimeTerminated(TerminationDetails::ForcedUnwind)) => (), + Err(e) => panic!("unexpected error: {}", e), + } + + // we've unwound the stack, so we know there are no longer any host frames. + debug_assert_eq!(inst.hostcall_count, 0); + + Ok(()) + }) + } + st => lucet_bail!("should never do forced unwinding in this state: {}", st), + } + } } /// Information about a runtime fault. @@ -1211,6 +1382,8 @@ pub struct FaultDetails { pub rip_addr: uintptr_t, /// Extra information about the instruction pointer's location, if available. pub rip_addr_details: Option, + /// Backtrace of the frames from the guest stack, if available. + pub backtrace: Option, } impl std::fmt::Display for FaultDetails { @@ -1265,6 +1438,8 @@ pub enum TerminationDetails { Provided(Box), /// The instance was terminated by its `KillSwitch`. Remote, + /// Returned when the stack is forced to unwind on instance reset or drop. + ForcedUnwind, } impl TerminationDetails { @@ -1315,6 +1490,7 @@ impl std::fmt::Debug for TerminationDetails { TerminationDetails::YieldTypeMismatch => write!(f, "YieldTypeMismatch"), TerminationDetails::Provided(_) => write!(f, "Provided(Any)"), TerminationDetails::Remote => write!(f, "Remote"), + TerminationDetails::ForcedUnwind => write!(f, "ForcedUnwind"), } } } diff --git a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs index 04b092da0..a009a5be6 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs @@ -5,6 +5,7 @@ use crate::instance::{ HOST_CTX, }; use crate::sysdeps::UContextPtr; +use backtrace::Backtrace; use lazy_static::lazy_static; use libc::{c_int, c_void, siginfo_t, SIGBUS, SIGSEGV}; use lucet_module::TrapCode; @@ -121,9 +122,9 @@ fn decrement_lucet_signal_state() { } impl Instance { - pub(crate) fn with_signals_on(&mut self, f: F) -> Result + pub(crate) fn with_signals_on(&mut self, f: F) -> Result<(), Error> where - F: FnOnce(&mut Instance) -> Result, + F: FnOnce(&mut Instance) -> Result<(), Error>, { let previous_sigstack = if self.ensure_sigstack_installed { validate_sigstack_size(self.alloc.slot().limits.signal_stack_size)?; @@ -173,8 +174,21 @@ impl Instance { ); } - // run the body - let res = f(self); + let res = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // run the body + f(self) + })) { + Ok(res) => res, + Err(e) => match e.downcast::() { + Ok(details) => { + self.state = State::Terminating { details: *details }; + Ok(()) + } + Err(e) => { + std::panic::resume_unwind(e); + } + }, + }; if self.ensure_signal_handler_installed { decrement_lucet_signal_state(); @@ -315,9 +329,11 @@ extern "C" fn handle_signal(signum: c_int, siginfo_ptr: *mut siginfo_t, ucontext // Details set to `None` here: have to wait until `verify_trap_safety` to // fill in these details, because access may not be signal safe. rip_addr_details: None, + backtrace: None, }, siginfo, context: ctx.into(), + full_backtrace: Backtrace::new_unresolved(), }; }; diff --git a/lucet-runtime/lucet-runtime-internals/src/instance/state.rs b/lucet-runtime/lucet-runtime-internals/src/instance/state.rs index bb77d52da..63f87937e 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance/state.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance/state.rs @@ -1,6 +1,7 @@ use crate::instance::siginfo_ext::SiginfoExt; use crate::instance::{FaultDetails, TerminationDetails, YieldedVal}; use crate::sysdeps::UContext; +use backtrace::Backtrace; use libc::{SIGBUS, SIGSEGV}; use std::any::Any; use std::ffi::{CStr, CString}; @@ -32,6 +33,7 @@ pub enum State { details: FaultDetails, siginfo: libc::siginfo_t, context: UContext, + full_backtrace: Backtrace, }, /// The instance is in the process of terminating. diff --git a/lucet-runtime/lucet-runtime-internals/src/lib.rs b/lucet-runtime/lucet-runtime-internals/src/lib.rs index 6737f9b3a..66ddc4c68 100644 --- a/lucet-runtime/lucet-runtime-internals/src/lib.rs +++ b/lucet-runtime/lucet-runtime-internals/src/lib.rs @@ -2,6 +2,7 @@ //! WebAssembly modules in lightweight sandboxes. It is intended to work with modules compiled by //! [`lucetc`](https://github.com/fastly/lucet/tree/master/lucetc). +#![feature(unwind_attributes)] #![deny(bare_trait_objects)] #[macro_use] diff --git a/lucet-runtime/lucet-runtime-internals/src/module.rs b/lucet-runtime/lucet-runtime-internals/src/module.rs index f0d78821b..56a2db374 100644 --- a/lucet-runtime/lucet-runtime-internals/src/module.rs +++ b/lucet-runtime/lucet-runtime-internals/src/module.rs @@ -11,6 +11,7 @@ pub use lucet_module::{ use crate::alloc::Limits; use crate::error::Error; +use backtrace::Backtrace; use libc::c_void; /// Details about a program address. @@ -69,6 +70,8 @@ pub trait ModuleInternal: Send + Sync { fn addr_details(&self, addr: *const c_void) -> Result, Error>; + fn resolve_and_trim(&self, full_bt: &Backtrace) -> Backtrace; + fn get_signature(&self, fn_id: FunctionIndex) -> &Signature; fn function_handle_from_ptr(&self, ptr: FunctionPointer) -> FunctionHandle { diff --git a/lucet-runtime/lucet-runtime-internals/src/module/dl.rs b/lucet-runtime/lucet-runtime-internals/src/module/dl.rs index f73497d8b..e8e25e1ef 100644 --- a/lucet-runtime/lucet-runtime-internals/src/module/dl.rs +++ b/lucet-runtime/lucet-runtime-internals/src/module/dl.rs @@ -1,5 +1,6 @@ use crate::error::Error; use crate::module::{AddrDetails, GlobalSpec, HeapSpec, Module, ModuleInternal, TableElement}; +use backtrace::{Backtrace, BacktraceFrame}; use libc::c_void; use libloading::Library; use lucet_module::{ @@ -331,6 +332,50 @@ impl ModuleInternal for DlModule { fn get_signature(&self, fn_id: FunctionIndex) -> &Signature { self.module.module_data.get_signature(fn_id) } + + fn resolve_and_trim(&self, full_bt: &Backtrace) -> Backtrace { + let mut bt = full_bt.clone(); + bt.resolve(); + let trimmed_frames = bt + .frames() + .iter() + .filter(|fr| match self.addr_details(fr.ip()) { + // if we can look up addr details, and it's in module code, keep the frame + Ok(Some(details)) => details.in_module_code, + _ => false, + }) + // // skip everything until the entry to the signal handler + // .skip_while(|fr| { + // fr.symbols() + // .iter() + // .find(|sym| { + // sym.name().map_or(false, |sn| { + // let name = format!("{}", sn); + // name.starts_with( + // "lucet_runtime_internals::instance::signals::handle_signal", + // ) && !name.contains("closure") + // }) + // }) + // .is_none() + // }) + // // drop the handle_signal frame + // .skip(1) + // // take all frames between handle_signal and Context::swap + // .take_while(|fr| { + // fr.symbols() + // .iter() + // .find(|sym| { + // sym.name().map_or(false, |sn| { + // format!("{}", sn) + // .starts_with("lucet_runtime_internals::context::Context::swap") + // }) + // }) + // .is_none() + // }) + .cloned() + .collect::>(); + trimmed_frames.into() + } } // TODO: PR to nix or libloading? diff --git a/lucet-runtime/lucet-runtime-internals/src/module/mock.rs b/lucet-runtime/lucet-runtime-internals/src/module/mock.rs index c64d2ef2d..d4930c9ac 100644 --- a/lucet-runtime/lucet-runtime-internals/src/module/mock.rs +++ b/lucet-runtime/lucet-runtime-internals/src/module/mock.rs @@ -1,5 +1,6 @@ use crate::error::Error; use crate::module::{AddrDetails, GlobalSpec, HeapSpec, Module, ModuleInternal, TableElement}; +use backtrace::Backtrace; use libc::c_void; use lucet_module::owned::{ OwnedExportFunction, OwnedFunctionMetadata, OwnedGlobalSpec, OwnedImportFunction, @@ -335,6 +336,14 @@ impl ModuleInternal for MockModule { Ok(None) } + fn resolve_and_trim(&self, full_bt: &Backtrace) -> Backtrace { + // for a mock module, just resolve since we can't differentiate between hostcall code and + // mock module functions + let mut bt = full_bt.clone(); + bt.resolve(); + bt + } + fn get_signature(&self, fn_id: FunctionIndex) -> &Signature { self.module_data.get_signature(fn_id) } diff --git a/lucet-runtime/lucet-runtime-internals/src/region.rs b/lucet-runtime/lucet-runtime-internals/src/region.rs index c72ae159f..8083fd3e8 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region.rs @@ -72,6 +72,10 @@ pub trait RegionInternal: Send + Sync { fn get_limits(&self) -> &Limits; fn as_dyn_internal(&self) -> &dyn RegionInternal; + + fn enable_stack_redzone(&self, slot: &Slot); + + fn disable_stack_redzone(&self, slot: &Slot); } /// A trait for regions that are created with a fixed capacity and limits. diff --git a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs index 99196d4b3..87de08782 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs @@ -276,6 +276,28 @@ impl RegionInternal for MmapRegion { fn as_dyn_internal(&self) -> &dyn RegionInternal { self } + + fn enable_stack_redzone(&self, slot: &Slot) { + unsafe { + mprotect( + slot.stack_redzone_start(), + host_page_size(), + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + ) + .expect("can set permissions on stack redzone page") + } + } + + fn disable_stack_redzone(&self, slot: &Slot) { + unsafe { + mprotect( + slot.stack_redzone_start(), + host_page_size(), + ProtFlags::PROT_NONE, + ) + .expect("can set permissions on stack redzone page") + } + } } impl Drop for MmapRegion { @@ -412,6 +434,7 @@ impl MmapRegion { sigstack: sigstack as *mut c_void, limits: region.limits.clone(), region: Arc::downgrade(region) as Weak, + redzone_stack_enabled: false, }) } diff --git a/lucet-runtime/lucet-runtime-internals/src/region/uffd.rs b/lucet-runtime/lucet-runtime-internals/src/region/uffd.rs index c1ef015df..f69e5907d 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region/uffd.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region/uffd.rs @@ -318,6 +318,28 @@ impl RegionInternal for UffdRegion { fn as_dyn_internal(&self) -> &dyn RegionInternal { self } + + fn enable_stack_redzone(&self, slot: &Slot) { + // The stack grows downward, `slot.stack` is the lowest address of the stack, meaning the + // guard page is before `slot.stack`. + let stack_guard_start = slot.stack as usize - host_page_size(); + unsafe { + self.uffd + .zeropage(stack_guard_start as *mut _, host_page_size(), true) + .expect("uffd.zeropage succeeds"); + } + } + + fn disable_stack_redzone(&self, slot: &Slot) { + let stack_guard_start = slot.stack as usize - host_page_size(); + unsafe { + madvise( + stack_guard_start as *mut _, + host_page_size(), + MmapAdvise::MADV_DONTNEED, + ).expect("disabling stack redzone succeeds"); + } + } } impl RegionCreate for UffdRegion { @@ -502,6 +524,7 @@ impl UffdRegion { sigstack: sigstack as *mut c_void, limits: region.limits.clone(), region: Arc::downgrade(region) as Weak, + redzone_stack_enabled: false, }) } } diff --git a/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs b/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs index da58e43cb..c7273e1b2 100644 --- a/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs +++ b/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs @@ -1,4 +1,9 @@ -use libc::{c_void, ucontext_t, REG_RDI, REG_RIP}; +use crate::context::Context; +use libc::{ + c_void, ucontext_t, REG_R12, REG_R13, REG_R14, REG_R15, REG_RBP, REG_RBX, REG_RDI, REG_RIP, + REG_RSP, +}; +use std::arch::x86_64::{__m128, _mm_loadu_ps}; #[derive(Clone, Copy, Debug)] pub struct UContextPtr(*mut ucontext_t); @@ -27,8 +32,44 @@ impl UContextPtr { let mut mcontext = &mut unsafe { self.0.as_mut().unwrap() }.uc_mcontext; mcontext.gregs[REG_RDI as usize] = new_rdi as i64; } + + pub fn save_to_context(&self, ctx: &mut Context) { + let mcontext = &unsafe { *(self.0) }.uc_mcontext; + ctx.gpr.rbx = mcontext.gregs[REG_RBX as usize] as u64; + ctx.gpr.rsp = mcontext.gregs[REG_RSP as usize] as u64; + ctx.gpr.rbp = mcontext.gregs[REG_RBP as usize] as u64; + ctx.gpr.rdi = mcontext.gregs[REG_RDI as usize] as u64; + ctx.gpr.r12 = mcontext.gregs[REG_R12 as usize] as u64; + ctx.gpr.r13 = mcontext.gregs[REG_R13 as usize] as u64; + ctx.gpr.r14 = mcontext.gregs[REG_R14 as usize] as u64; + ctx.gpr.r15 = mcontext.gregs[REG_R15 as usize] as u64; + + let fpregs = &unsafe { *(mcontext.fpregs) }; + let xmms = fpregs._xmm[0..8] + .iter() + .map(|reg| unsafe { _mm_loadu_ps(reg.element.as_ptr() as *const u32 as *const _) }) + .collect::>(); + ctx.fpr.xmm0 = xmms[0]; + ctx.fpr.xmm1 = xmms[1]; + ctx.fpr.xmm2 = xmms[2]; + ctx.fpr.xmm3 = xmms[3]; + ctx.fpr.xmm4 = xmms[4]; + ctx.fpr.xmm5 = xmms[5]; + ctx.fpr.xmm6 = xmms[6]; + ctx.fpr.xmm7 = xmms[7]; + } } +// TODO: refactor uses of these types so that a deref instance can make sense, then move the methods +// from the ptr type into the target-specific ucontext type +// +// impl std::ops::Deref for UContextPtr { +// type Target = UContext; +// fn deref(&self) -> &Self::Target { +// &unsafe { UContext::new(self.0 as *const c_void) } +// } +// } + #[repr(C)] #[derive(Clone, Copy)] pub struct UContext { diff --git a/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs b/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs index ebcae4cdf..b03701f39 100644 --- a/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs +++ b/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs @@ -1,4 +1,6 @@ +use crate::context::Context; use libc::{c_int, c_short, c_void, sigset_t, size_t}; +use std::arch::x86_64::{__m128, _mm_loadu_ps}; #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct sigaltstack { @@ -148,6 +150,41 @@ impl UContextPtr { let mcontext: &mut mcontext64 = unsafe { &mut (*self.0).uc_mcontext.as_mut().unwrap() }; mcontext.ss.rdi = new_rdi; } + + pub fn save_to_context(&self, ctx: &mut Context) { + let mcontext: &mut mcontext64 = unsafe { (*self.0).uc_mcontext.as_mut().unwrap() }; + ctx.gpr.rbx = mcontext.ss.rbx; + ctx.gpr.rsp = mcontext.ss.rsp; + ctx.gpr.rbp = mcontext.ss.rbp; + ctx.gpr.rdi = mcontext.ss.rdi; + ctx.gpr.r12 = mcontext.ss.r12; + ctx.gpr.r13 = mcontext.ss.r13; + ctx.gpr.r14 = mcontext.ss.r14; + ctx.gpr.r15 = mcontext.ss.r15; + + let fpregs = &mcontext.fs; + let xmms = [ + fpregs.fpu_xmm0, + fpregs.fpu_xmm1, + fpregs.fpu_xmm2, + fpregs.fpu_xmm3, + fpregs.fpu_xmm4, + fpregs.fpu_xmm5, + fpregs.fpu_xmm6, + fpregs.fpu_xmm7, + ] + .iter() + .map(|reg| unsafe { _mm_loadu_ps(reg.0.as_ptr() as *const u32 as *const _) }) + .collect::>(); + ctx.fpr.xmm0 = xmms[0]; + ctx.fpr.xmm1 = xmms[1]; + ctx.fpr.xmm2 = xmms[2]; + ctx.fpr.xmm3 = xmms[3]; + ctx.fpr.xmm4 = xmms[4]; + ctx.fpr.xmm5 = xmms[5]; + ctx.fpr.xmm6 = xmms[6]; + ctx.fpr.xmm7 = xmms[7]; + } } #[derive(Clone, Copy)] diff --git a/lucet-runtime/lucet-runtime-internals/src/vmctx.rs b/lucet-runtime/lucet-runtime-internals/src/vmctx.rs index 981b5be92..308bccef2 100644 --- a/lucet-runtime/lucet-runtime-internals/src/vmctx.rs +++ b/lucet-runtime/lucet-runtime-internals/src/vmctx.rs @@ -295,6 +295,7 @@ impl Vmctx { /// [ongoing](https://github.com/bytecodealliance/lucet/pull/254). /// /// ```no_run + /// # #![feature(unwind_attributes)] /// use lucet_runtime_macros::lucet_hostcall; /// use lucet_runtime_internals::lucet_hostcall_terminate; /// use lucet_runtime_internals::vmctx::{lucet_vmctx, Vmctx}; @@ -397,6 +398,9 @@ impl Vmctx { }; HOST_CTX.with(|host_ctx| unsafe { Context::swap(&mut inst.ctx, &mut *host_ctx.get()) }); + if let Some(td) = inst.pending_termination.take() { + panic!(td); + } } /// Take and return the value passed to diff --git a/lucet-runtime/lucet-runtime-macros/src/lib.rs b/lucet-runtime/lucet-runtime-macros/src/lib.rs index fd575946f..c0d15a31a 100644 --- a/lucet-runtime/lucet-runtime-macros/src/lib.rs +++ b/lucet-runtime/lucet-runtime-macros/src/lib.rs @@ -90,12 +90,6 @@ pub fn lucet_hostcall(_attr: TokenStream, item: TokenStream) -> TokenStream { }) .collect::>(); - let termination_details = if from_internals { - quote! { lucet_runtime_internals::instance::TerminationDetails } - } else { - quote! { lucet_runtime::TerminationDetails } - }; - let raw_hostcall = quote! { #(#attrs)* #vis @@ -105,20 +99,10 @@ pub fn lucet_hostcall(_attr: TokenStream, item: TokenStream) -> TokenStream { let vmctx = #vmctx_mod::Vmctx::from_raw(vmctx_raw); #vmctx_mod::VmctxInternal::instance_mut(&vmctx).uninterruptable(|| { - let res = std::panic::catch_unwind(move || { - #hostcall_ident(&#vmctx_mod::Vmctx::from_raw(vmctx_raw), #(#impl_args),*) - }); - match res { - Ok(res) => res, - Err(e) => { - match e.downcast::<#termination_details>() { - Ok(details) => { - #vmctx_mod::Vmctx::from_raw(vmctx_raw).terminate_no_unwind(*details) - }, - Err(e) => std::panic::resume_unwind(e), - } - } - } + let vmctx = #vmctx_mod::Vmctx::from_raw(vmctx_raw); + #vmctx_mod::VmctxInternal::instance_mut(&vmctx).in_hostcall(|| { + #hostcall_ident(&mut #vmctx_mod::Vmctx::from_raw(vmctx_raw), #(#impl_args),*) + }) }) } }; diff --git a/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json b/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json index 383bcf316..8a44915a3 100644 --- a/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json +++ b/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json @@ -1,7 +1,15 @@ { "env": { + "bad_access_unwind": "hostcall_bad_access_unwind", + "stack_overflow_unwind": "hostcall_stack_overflow_unwind", "hostcall_test_func_hello": "hostcall_test_func_hello", "hostcall_test_func_hostcall_error": "hostcall_test_func_hostcall_error", - "hostcall_test_func_hostcall_error_unwind": "hostcall_test_func_hostcall_error_unwind" + "hostcall_test_func_hostcall_error_unwind": "hostcall_test_func_hostcall_error_unwind", + "nested_error_unwind_outer": "nested_error_unwind_outer", + "nested_error_unwind_inner": "nested_error_unwind_inner", + "nested_error_unwind_regs_outer": "nested_error_unwind_regs_outer", + "nested_error_unwind_regs_inner": "nested_error_unwind_regs_inner", + "restore_callee_saved": "hostcall_restore_callee_saved", + "hostcall_panic": "hostcall_panic" } } diff --git a/lucet-runtime/lucet-runtime-tests/guests/host/fault_unwind.c b/lucet-runtime/lucet-runtime-tests/guests/host/fault_unwind.c new file mode 100644 index 000000000..da01e11ed --- /dev/null +++ b/lucet-runtime/lucet-runtime-tests/guests/host/fault_unwind.c @@ -0,0 +1,25 @@ +#include +#include + +extern void bad_access_unwind(void (*)(void)); +extern void stack_overflow_unwind(void (*)(void)); + +void do_bad_access(void) +{ + *(uint64_t *) 0xFFFFFFFF = 420; + return; +} + +void bad_access(void) +{ + return bad_access_unwind(do_bad_access); +} + +void do_stack_overflow(void) { + do_stack_overflow(); +} + +void stack_overflow(void) +{ + return stack_overflow_unwind(do_stack_overflow); +} diff --git a/lucet-runtime/lucet-runtime-tests/guests/host/nested_error_unwind.c b/lucet-runtime/lucet-runtime-tests/guests/host/nested_error_unwind.c new file mode 100644 index 000000000..1a9e09392 --- /dev/null +++ b/lucet-runtime/lucet-runtime-tests/guests/host/nested_error_unwind.c @@ -0,0 +1,121 @@ +#include +#include + +extern uint64_t nested_error_unwind_outer(uint64_t (*)(void)); +extern void nested_error_unwind_inner(); + +uint64_t callback(void) +{ + nested_error_unwind_inner(); + return 0; +} + +uint64_t entrypoint(void) +{ + return nested_error_unwind_outer(callback); +} + +extern uint64_t nested_error_unwind_regs_outer(uint64_t (*)(void)); +extern void nested_error_unwind_regs_inner(); + +uint64_t callback_regs(void) +{ + uint64_t a = 0xFFFFFFFF00000000; + uint64_t b = 0xFFFFFFFF00000001; + uint64_t c = 0xFFFFFFFF00000002; + uint64_t d = 0xFFFFFFFF00000003; + uint64_t e = 0xFFFFFFFF00000004; + uint64_t f = 0xFFFFFFFF00000005; + uint64_t g = 0xFFFFFFFF00000006; + uint64_t h = 0xFFFFFFFF00000007; + uint64_t i = 0xFFFFFFFF00000008; + uint64_t j = 0xFFFFFFFF00000009; + uint64_t k = 0xFFFFFFFF0000000A; + uint64_t l = 0xFFFFFFFF0000000B; + + a = b + c ^ 0; + b = c + d ^ 1; + c = d + e ^ 2; + d = e + f ^ 3; + e = f + g ^ 4; + f = g + h ^ 5; + g = h + i ^ 6; + h = i + j ^ 7; + i = j + k ^ 8; + j = k + l ^ 9; + k = l + a ^ 10; + l = a + b ^ 11; + + nested_error_unwind_regs_inner(); + + a = b * c & 0; + b = c * d & 1; + c = d * e & 2; + d = e * f & 3; + e = f * g & 4; + f = g * h & 5; + g = h * i & 6; + h = i * j & 7; + i = j * k & 8; + j = k * l & 9; + k = l * a & 10; + l = a * b & 11; + return l; +} + +uint64_t entrypoint_regs(void) +{ + return nested_error_unwind_regs_outer(callback_regs); +} + +extern uint64_t restore_callee_saved(uint64_t (*)(void)); +extern void hostcall_panic(); + +uint64_t callback_restore(void) +{ + uint64_t a = 0xFFFFFFFF00000000; + uint64_t b = 0xFFFFFFFF00000001; + uint64_t c = 0xFFFFFFFF00000002; + uint64_t d = 0xFFFFFFFF00000003; + uint64_t e = 0xFFFFFFFF00000004; + uint64_t f = 0xFFFFFFFF00000005; + uint64_t g = 0xFFFFFFFF00000006; + uint64_t h = 0xFFFFFFFF00000007; + uint64_t i = 0xFFFFFFFF00000008; + uint64_t j = 0xFFFFFFFF00000009; + uint64_t k = 0xFFFFFFFF0000000A; + uint64_t l = 0xFFFFFFFF0000000B; + + a = b + c ^ 0; + b = c + d ^ 1; + c = d + e ^ 2; + d = e + f ^ 3; + e = f + g ^ 4; + f = g + h ^ 5; + g = h + i ^ 6; + h = i + j ^ 7; + i = j + k ^ 8; + j = k + l ^ 9; + k = l + a ^ 10; + l = a + b ^ 11; + + hostcall_panic(); + + a = b * c & 0; + b = c * d & 1; + c = d * e & 2; + d = e * f & 3; + e = f * g & 4; + f = g * h & 5; + g = h * i & 6; + h = i * j & 7; + i = j * k & 8; + j = k * l & 9; + k = l * a & 10; + l = a * b & 11; + return l; +} + +uint64_t entrypoint_restore(void) { + return restore_callee_saved(callback_restore); +} diff --git a/lucet-runtime/lucet-runtime-tests/src/build.rs b/lucet-runtime/lucet-runtime-tests/src/build.rs index 6a22fc21d..642a1b91d 100644 --- a/lucet-runtime/lucet-runtime-tests/src/build.rs +++ b/lucet-runtime/lucet-runtime-tests/src/build.rs @@ -50,6 +50,9 @@ where let dlmodule = DlModule::load(so_file)?; + // temporary, so that gdb doesn't barf due to the .so being deleted too early + workdir.into_path(); + Ok(dlmodule) } diff --git a/lucet-runtime/lucet-runtime-tests/src/host.rs b/lucet-runtime/lucet-runtime-tests/src/host.rs index 8b460fedb..d5d12fef2 100644 --- a/lucet-runtime/lucet-runtime-tests/src/host.rs +++ b/lucet-runtime/lucet-runtime-tests/src/host.rs @@ -27,6 +27,39 @@ macro_rules! host_tests { const ERROR_MESSAGE: &'static str = "hostcall_test_func_hostcall_error"; + lazy_static! { + static ref NESTED_OUTER: Mutex<()> = Mutex::new(()); + static ref NESTED_INNER: Mutex<()> = Mutex::new(()); + static ref NESTED_REGS_OUTER: Mutex<()> = Mutex::new(()); + static ref NESTED_REGS_INNER: Mutex<()> = Mutex::new(()); + } + + static mut HOSTCALL_MUTEX: Option> = None; + static mut BAD_ACCESS_UNWIND: Option> = None; + static mut STACK_OVERFLOW_UNWIND: Option> = None; + + #[allow(unreachable_code)] + #[inline] + unsafe fn unwind_inner(vmctx: &Vmctx, mutex: &Mutex<()>) { + let lock = mutex.lock().unwrap(); + lucet_hostcall_terminate!(ERROR_MESSAGE); + drop(lock); + } + + #[inline] + unsafe fn unwind_outer(vmctx: &Vmctx, mutex: &Mutex<()>, cb_idx: u32) -> u64 { + let lock = mutex.lock().unwrap(); + let func = vmctx + .get_func_from_idx(0, cb_idx) + .expect("can get function by index"); + let func = std::mem::transmute:: u64>( + func.ptr.as_usize(), + ); + let res = (func)(vmctx.as_raw()); + drop(lock); + res + } + #[lucet_hostcall] #[no_mangle] pub fn hostcall_test_func_hostcall_error(_vmctx: &Vmctx) { @@ -52,10 +85,167 @@ macro_rules! host_tests { #[allow(unreachable_code)] #[no_mangle] pub fn hostcall_test_func_hostcall_error_unwind(vmctx: &Vmctx) { - let lock = vmctx.get_embed_ctx::>>(); - let _mutex_guard = lock.lock().unwrap(); - lucet_hostcall_terminate!(ERROR_MESSAGE); - drop(_mutex_guard); + let lock = unsafe { HOSTCALL_MUTEX.as_ref().unwrap() }.lock().unwrap(); + unsafe { + lucet_hostcall_terminate!(ERROR_MESSAGE); + } + drop(lock); + } + + #[lucet_hostcall] + #[no_mangle] + pub fn nested_error_unwind_outer( + vmctx: &Vmctx, + cb_idx: u32, + ) -> u64 { + unsafe { + unwind_outer(vmctx, &*NESTED_OUTER, cb_idx) + } + } + + #[lucet_hostcall] + #[no_mangle] + pub fn nested_error_unwind_inner( + vmctx: &Vmctx, + ) -> () { + unsafe { + unwind_inner(vmctx, &*NESTED_INNER) + } + } + + #[lucet_hostcall] + #[no_mangle] + pub fn nested_error_unwind_regs_outer( + vmctx: &Vmctx, + cb_idx: u32, + ) -> u64 { + unsafe { + unwind_outer(vmctx, &*NESTED_REGS_OUTER, cb_idx) + } + } + + #[lucet_hostcall] + #[no_mangle] + pub fn nested_error_unwind_regs_inner( + vmctx: &Vmctx, + ) -> () { + unsafe { + unwind_inner(vmctx, &*NESTED_REGS_INNER) + } + } + + #[lucet_hostcall] + #[no_mangle] + pub fn hostcall_panic( + _vmctx: &Vmctx, + ) -> () { + panic!("hostcall_panic"); + } + + #[lucet_hostcall] + #[no_mangle] + pub fn hostcall_restore_callee_saved( + vmctx: &Vmctx, + cb_idx: u32, + ) -> u64 { + let mut a: u64; + let mut b: u64 = 0xAAAAAAAA00000001; + let mut c: u64 = 0xAAAAAAAA00000002; + let mut d: u64 = 0xAAAAAAAA00000003; + let mut e: u64 = 0xAAAAAAAA00000004; + let mut f: u64 = 0xAAAAAAAA00000005; + let mut g: u64 = 0xAAAAAAAA00000006; + let mut h: u64 = 0xAAAAAAAA00000007; + let mut i: u64 = 0xAAAAAAAA00000008; + let mut j: u64 = 0xAAAAAAAA00000009; + let mut k: u64 = 0xAAAAAAAA0000000A; + let mut l: u64 = 0xAAAAAAAA0000000B; + + a = b.wrapping_add(c ^ 0); + b = c.wrapping_add(d ^ 1); + c = d.wrapping_add(e ^ 2); + d = e.wrapping_add(f ^ 3); + e = f.wrapping_add(g ^ 4); + f = g.wrapping_add(h ^ 5); + g = h.wrapping_add(i ^ 6); + h = i.wrapping_add(j ^ 7); + i = j.wrapping_add(k ^ 8); + j = k.wrapping_add(l ^ 9); + k = l.wrapping_add(a ^ 10); + l = a.wrapping_add(b ^ 11); + + let func = vmctx + .get_func_from_idx(0, cb_idx) + .expect("can get function by index"); + let func = unsafe { + std::mem::transmute:: u64>( + func.ptr.as_usize(), + ) + }; + let vmctx_raw = vmctx.as_raw(); + let res = std::panic::catch_unwind(|| { + (func)(vmctx_raw); + }); + assert!(res.is_err()); + + a = b.wrapping_mul(c & 0); + b = c.wrapping_mul(d & 1); + c = d.wrapping_mul(e & 2); + d = e.wrapping_mul(f & 3); + e = f.wrapping_mul(g & 4); + f = g.wrapping_mul(h & 5); + g = h.wrapping_mul(i & 6); + h = i.wrapping_mul(j & 7); + i = j.wrapping_mul(k & 8); + j = k.wrapping_mul(l & 9); + k = l.wrapping_mul(a & 10); + l = a.wrapping_mul(b & 11); + + a ^ b ^ c ^ d ^ e ^ f ^ g ^ h ^ i ^ j ^ k ^ l + } + + #[lucet_hostcall] + #[no_mangle] + pub fn hostcall_stack_overflow_unwind( + vmctx: &Vmctx, + cb_idx: u32, + ) -> () { + let lock = unsafe { STACK_OVERFLOW_UNWIND.as_ref().unwrap() }.lock().unwrap(); + + let func = vmctx + .get_func_from_idx(0, cb_idx) + .expect("can get function by index"); + let func = unsafe { + std::mem::transmute::( + func.ptr.as_usize(), + ) + }; + let vmctx_raw = vmctx.as_raw(); + func(vmctx_raw); + + drop(lock); + } + + #[lucet_hostcall] + #[no_mangle] + pub fn hostcall_bad_access_unwind( + vmctx: &Vmctx, + cb_idx: u32, + ) -> () { + let lock = unsafe { BAD_ACCESS_UNWIND.as_ref().unwrap() }.lock().unwrap(); + + let func = vmctx + .get_func_from_idx(0, cb_idx) + .expect("can get function by index"); + let func = unsafe { + std::mem::transmute::( + func.ptr.as_usize(), + ) + }; + let vmctx_raw = vmctx.as_raw(); + func(vmctx_raw); + + drop(lock); } #[lucet_hostcall] @@ -186,7 +376,6 @@ macro_rules! host_tests { $( mod $region_id { - use lazy_static::lazy_static; use libc::c_void; use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; @@ -196,13 +385,9 @@ macro_rules! host_tests { }; use std::sync::{Arc, Mutex}; use $crate::build::test_module_c; - use $crate::helpers::{FunctionPointer, HeapSpec, MockExportBuilder, MockModuleBuilder}; + use $crate::helpers::{FunctionPointer, HeapSpec, MockExportBuilder, MockModuleBuilder, test_ex}; use $TestRegion as TestRegion; - lazy_static! { - static ref HOSTCALL_MUTEX: Arc> = Arc::new(Mutex::new(())); - } - #[test] fn load_module() { let _module = test_module_c("host", "trivial.c").expect("build and load module"); @@ -276,17 +461,97 @@ macro_rules! host_tests { #[test] fn run_hostcall_error_unwind() { + test_ex(|| { + // Since `hostcall_test_func_hostcall_error_unwind` is reused in two + // different modules, meaning two different tests, we need to reset the + // mutex it will (hopefully) poison before running this test. + // + // The contention for this global mutex is why this test must be `test_ex`. + unsafe { + super::HOSTCALL_MUTEX = Some(Mutex::new(())); + } + + let module = + test_module_c("host", "hostcall_error_unwind.c").expect("build and load module"); + let region = ::create(1, &Limits::default()).expect("region can be created"); + + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + match inst.run("main", &[0u32.into(), 0u32.into()]) { + Err(Error::RuntimeTerminated(term)) => { + assert_eq!( + *term + .provided_details() + .expect("user provided termination reason") + .downcast_ref::<&'static str>() + .expect("error was static str"), + super::ERROR_MESSAGE + ); + } + res => panic!("unexpected result: {:?}", res), + } + + unsafe { + assert!(super::HOSTCALL_MUTEX.as_ref().unwrap().is_poisoned()); + } + }) + } + + /// Check that if two segments of hostcall stack are present when terminating, that they + /// both get properly unwound. + /// + /// Currently ignored as we don't allow nested hostcalls - the nested hostcall runs afoul + /// of timeouts' domain-checking logic, which assumes beginning a hostscall will only + /// happen from a guest context, but when initiated from a nested hostcall is actually a + /// hostcall context + #[test] + #[ignore] + fn nested_error_unwind() { let module = - test_module_c("host", "hostcall_error_unwind.c").expect("build and load module"); + test_module_c("host", "nested_error_unwind.c").expect("build and load module"); let region = ::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + match inst.run("entrypoint", &[]) { + Err(Error::RuntimeTerminated(term)) => { + assert_eq!( + *term + .provided_details() + .expect("user provided termination reason") + .downcast_ref::<&'static str>() + .expect("error was static str"), + super::ERROR_MESSAGE + ); + } + res => panic!("unexpected result: {:?}", res), + } + + assert!(super::NESTED_OUTER.is_poisoned()); + assert!(super::NESTED_INNER.is_poisoned()); + } + + /// Like `nested_error_unwind`, but the guest code callback in between the two segments of + /// hostcall stack uses enough locals to require saving callee registers. + /// + /// Currently ignored as we don't allow nested hostcalls - the nested hostcall runs afoul + /// of timeouts' domain-checking logic, which assumes beginning a hostscall will only + /// happen from a guest context, but when initiated from a nested hostcall is actually a + /// hostcall context + #[test] + #[ignore] + fn nested_error_unwind_regs() { + let module = + test_module_c("host", "nested_error_unwind.c").expect("build and load module"); + let region = ::create(1, &Limits::default()).expect("region can be created"); let mut inst = region - .new_instance_builder(module) - .with_embed_ctx(HOSTCALL_MUTEX.clone()) - .build() + .new_instance(module) .expect("instance can be created"); - match inst.run("main", &[0u32.into(), 0u32.into()]) { + match inst.run("entrypoint_regs", &[]) { Err(Error::RuntimeTerminated(term)) => { assert_eq!( *term @@ -300,7 +565,69 @@ macro_rules! host_tests { res => panic!("unexpected result: {:?}", res), } - assert!(HOSTCALL_MUTEX.is_poisoned()); + assert!(super::NESTED_REGS_OUTER.is_poisoned()); + assert!(super::NESTED_REGS_INNER.is_poisoned()); + } + + /// Ensures that callee-saved registers are properly restored following a `catch_unwind` + /// that catches a panic. + /// + /// Currently ignored as we don't allow nested hostcalls - the nested hostcall runs afoul + /// of timeouts' domain-checking logic, which assumes beginning a hostscall will only + /// happen from a guest context, but when initiated from a nested hostcall is actually a + /// hostcall context + #[ignore] + #[test] + fn restore_callee_saved() { + let module = + test_module_c("host", "nested_error_unwind.c").expect("build and load module"); + let region = ::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + assert_eq!( + u64::from(inst.run("entrypoint_restore", &[]).unwrap().unwrap_returned()), + 6148914668330025056 + ); + } + + /// Ensures that hostcall stack frames get unwound when a fault occurs in guest code. + #[test] + fn bad_access_unwind() { + test_ex(|| { + unsafe { + super::BAD_ACCESS_UNWIND = Some(Mutex::new(())); + } + let module = test_module_c("host", "fault_unwind.c").expect("build and load module"); + let region = ::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + inst.run("bad_access", &[]).unwrap_err(); + inst.reset().unwrap(); + unsafe { + assert!(unsafe { super::BAD_ACCESS_UNWIND.as_ref().unwrap() }.is_poisoned()); + } + }) + } + + /// Ensures that hostcall stack frames get unwound even when a stack overflow occurs in + /// guest code. + #[test] + fn stack_overflow_unwind() { + test_ex(|| { + unsafe { + super::STACK_OVERFLOW_UNWIND = Some(Mutex::new(())); + } + let module = test_module_c("host", "fault_unwind.c").expect("build and load module"); + let region = ::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + inst.run("stack_overflow", &[]).unwrap_err(); + inst.reset().unwrap(); + assert!(unsafe { super::STACK_OVERFLOW_UNWIND.as_ref().unwrap() }.is_poisoned()); + }) } #[test] diff --git a/lucet-runtime/src/lib.rs b/lucet-runtime/src/lib.rs index a710f746c..663912dc8 100644 --- a/lucet-runtime/src/lib.rs +++ b/lucet-runtime/src/lib.rs @@ -83,6 +83,7 @@ //! to make a `u32` available to hostcalls: //! //! ```no_run +//! # #![feature(unwind_attributes)] //! use lucet_runtime::{DlModule, Limits, MmapRegion, Region, lucet_hostcall}; //! use lucet_runtime::vmctx::{Vmctx, lucet_vmctx}; //! @@ -198,6 +199,7 @@ //! and yield it when appropriate. //! //! ```no_run +//! # #![feature(unwind_attributes)] //! use lucet_runtime::lucet_hostcall; //! use lucet_runtime::vmctx::Vmctx; //! @@ -395,6 +397,7 @@ //! lucet-runtime-internals = { version = "0.6.1", default-features = false } //! ``` +#![feature(unwind_attributes)] #![deny(bare_trait_objects)] // This makes `lucet_runtime` in the expansion of `#[lucet_hostcall]` resolve to something diff --git a/lucet-runtime/tests/entrypoint.rs b/lucet-runtime/tests/entrypoint.rs index a3ecfb89a..c1728b46a 100644 --- a/lucet-runtime/tests/entrypoint.rs +++ b/lucet-runtime/tests/entrypoint.rs @@ -1,3 +1,5 @@ +#![feature(unwind_attributes)] + use lucet_runtime_tests::entrypoint_tests; cfg_if::cfg_if! { diff --git a/lucet-runtime/tests/guest_fault.rs b/lucet-runtime/tests/guest_fault.rs index bb4353c0a..12583bcba 100644 --- a/lucet-runtime/tests/guest_fault.rs +++ b/lucet-runtime/tests/guest_fault.rs @@ -1,3 +1,5 @@ +#![feature(unwind_attributes)] + use lucet_runtime_tests::{guest_fault_common_defs, guest_fault_tests}; guest_fault_common_defs!(); diff --git a/lucet-runtime/tests/host.rs b/lucet-runtime/tests/host.rs index 5404c2c15..19c6b8068 100644 --- a/lucet-runtime/tests/host.rs +++ b/lucet-runtime/tests/host.rs @@ -1,3 +1,5 @@ +#![feature(unwind_attributes)] + use lucet_runtime_tests::host_tests; cfg_if::cfg_if! { diff --git a/lucet-runtime/tests/strcmp.rs b/lucet-runtime/tests/strcmp.rs index badd163c1..a0a540995 100644 --- a/lucet-runtime/tests/strcmp.rs +++ b/lucet-runtime/tests/strcmp.rs @@ -1,3 +1,5 @@ +#![feature(unwind_attributes)] + use lucet_runtime_tests::strcmp_tests; cfg_if::cfg_if! { diff --git a/lucet-spectest/src/lib.rs b/lucet-spectest/src/lib.rs index 2b7191212..3b15f950a 100644 --- a/lucet-spectest/src/lib.rs +++ b/lucet-spectest/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(unwind_attributes)] #![deny(bare_trait_objects)] pub mod error; diff --git a/lucet-validate/Cargo.toml b/lucet-validate/Cargo.toml index e6f7c06ff..502204f64 100644 --- a/lucet-validate/Cargo.toml +++ b/lucet-validate/Cargo.toml @@ -19,7 +19,7 @@ path = "src/main.rs" [dependencies] clap = "2" witx = { path = "../wasmtime/crates/wasi-common/WASI/tools/witx", version = "0.8.5" } -cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.64.0" } +cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.65.0" } thiserror = "1.0.4" wasmparser = "0.52.0" diff --git a/lucet-wasi/Cargo.toml b/lucet-wasi/Cargo.toml index dd80248c1..74a39791d 100644 --- a/lucet-wasi/Cargo.toml +++ b/lucet-wasi/Cargo.toml @@ -22,7 +22,7 @@ lucet-wiggle = { path = "../lucet-wiggle", version = "=0.7.0-dev" } libc = "0.2.65" nix = "0.17" rand = "0.6" -wasi-common = { path = "../wasmtime/crates/wasi-common", version = "0.17.0", features = ["wiggle_metadata"] } +wasi-common = { path = "../wasmtime/crates/wasi-common", version = "0.18.0", features = ["wiggle_metadata"] } [dev-dependencies] lucet-wasi-sdk = { path = "../lucet-wasi-sdk" } diff --git a/lucet-wasi/generate/Cargo.toml b/lucet-wasi/generate/Cargo.toml index ba4889cab..e36394153 100644 --- a/lucet-wasi/generate/Cargo.toml +++ b/lucet-wasi/generate/Cargo.toml @@ -13,8 +13,8 @@ proc-macro = true [dependencies] lucet-wiggle = { path = "../../lucet-wiggle", version = "0.7.0-dev" } -wasi-common = { path = "../../wasmtime/crates/wasi-common", version = "0.17.0", features = ["wiggle_metadata"] } -wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.17.0" } +wasi-common = { path = "../../wasmtime/crates/wasi-common", version = "0.18.0", features = ["wiggle_metadata"] } +wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.18.0" } syn = { version = "1.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" diff --git a/lucet-wasi/src/lib.rs b/lucet-wasi/src/lib.rs index fc7281209..4d20eccef 100644 --- a/lucet-wasi/src/lib.rs +++ b/lucet-wasi/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(unwind_attributes)] #![deny(bare_trait_objects)] pub mod c_api; diff --git a/lucet-wiggle/Cargo.toml b/lucet-wiggle/Cargo.toml index 911f2fb14..38a9566a1 100644 --- a/lucet-wiggle/Cargo.toml +++ b/lucet-wiggle/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" lucet-wiggle-macro = { path = "./macro", version = "0.7.0-dev" } lucet-wiggle-generate = { path = "./generate", version = "0.7.0-dev" } lucet-runtime = { path = "../lucet-runtime", version = "0.7.0-dev" } -wiggle = { path = "../wasmtime/crates/wiggle", version = "0.17.0" } +wiggle = { path = "../wasmtime/crates/wiggle", version = "0.18.0" } [dev-dependencies] wiggle-test = { path = "../wasmtime/crates/wiggle/test-helpers" } diff --git a/lucet-wiggle/generate/Cargo.toml b/lucet-wiggle/generate/Cargo.toml index 2add756f1..fb6e8ed02 100644 --- a/lucet-wiggle/generate/Cargo.toml +++ b/lucet-wiggle/generate/Cargo.toml @@ -9,7 +9,7 @@ authors = ["Lucet team "] edition = "2018" [dependencies] -wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.17.0" } +wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.18.0" } lucet-module = { path = "../../lucet-module", version = "0.7.0-dev" } witx = { path = "../../wasmtime/crates/wasi-common/WASI/tools/witx", version = "0.8.4" } quote = "1.0" diff --git a/lucet-wiggle/macro/Cargo.toml b/lucet-wiggle/macro/Cargo.toml index dcfe9e778..b26e8377e 100644 --- a/lucet-wiggle/macro/Cargo.toml +++ b/lucet-wiggle/macro/Cargo.toml @@ -13,7 +13,7 @@ proc-macro = true [dependencies] lucet-wiggle-generate = { path = "../generate", version = "0.7.0-dev" } -wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.17.0" } +wiggle-generate = { path = "../../wasmtime/crates/wiggle/generate", version = "0.18.0" } witx = { path = "../../wasmtime/crates/wasi-common/WASI/tools/witx", version = "0.8.4" } syn = { version = "1.0", features = ["full"] } quote = "1.0" diff --git a/lucetc/Cargo.toml b/lucetc/Cargo.toml index 59bd9551b..6d3f16518 100644 --- a/lucetc/Cargo.toml +++ b/lucetc/Cargo.toml @@ -16,13 +16,13 @@ path = "lucetc/main.rs" [dependencies] anyhow = "1" bincode = "1.1.4" -cranelift-codegen = { path = "../wasmtime/cranelift/codegen", version = "0.64.0" } -cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.64.0" } -cranelift-native = { path = "../wasmtime/cranelift/native", version = "0.64.0" } -cranelift-frontend = { path = "../wasmtime/cranelift/frontend", version = "0.64.0" } -cranelift-module = { path = "../wasmtime/cranelift/module", version = "0.64.0" } -cranelift-object = { path = "../wasmtime/cranelift/object", version = "0.64.0" } -cranelift-wasm = { path = "../wasmtime/cranelift/wasm", version = "0.64.0" } +cranelift-codegen = { path = "../wasmtime/cranelift/codegen", version = "0.65.0", features = ["unwind"] } +cranelift-entity = { path = "../wasmtime/cranelift/entity", version = "0.65.0" } +cranelift-native = { path = "../wasmtime/cranelift/native", version = "0.65.0" } +cranelift-frontend = { path = "../wasmtime/cranelift/frontend", version = "0.65.0" } +cranelift-module = { path = "../wasmtime/cranelift/module", version = "0.65.0" } +cranelift-object = { path = "../wasmtime/cranelift/object", version = "0.65.0" } +cranelift-wasm = { path = "../wasmtime/cranelift/wasm", version = "0.65.0" } target-lexicon = "0.10" lucet-module = { path = "../lucet-module", version = "=0.7.0-dev" } lucet-validate = { path = "../lucet-validate", version = "=0.7.0-dev" } @@ -31,7 +31,8 @@ wasmparser = "0.57.0" clap="2.32" log = "0.4" env_logger = "0.6" -object = { version = "0.18.0", default-features = false, features = ["write"] } +gimli = { version = "0.21.0", default-features = false, features = ["write"] } +object = { version = "0.19.0", default-features = false, features = ["write"] } byteorder = "1.2" wabt = "0.9.2" tempfile = "3.0" diff --git a/lucetc/src/compiler.rs b/lucetc/src/compiler.rs index af85ed47f..b1d59c086 100644 --- a/lucetc/src/compiler.rs +++ b/lucetc/src/compiler.rs @@ -11,7 +11,10 @@ use crate::runtime::Runtime; use crate::stack_probe; use crate::table::write_table_data; use crate::traps::{translate_trapcode, trap_sym_for_func}; +use crate::unwind::EhFrameSink; use byteorder::{LittleEndian, WriteBytesExt}; +use cranelift_codegen::isa::unwind::UnwindInfo; +use cranelift_codegen::CodegenError; use cranelift_codegen::{ binemit, ir, isa::TargetIsa, @@ -20,10 +23,12 @@ use cranelift_codegen::{ }; use cranelift_module::{ Backend as ClifBackend, DataContext as ClifDataContext, DataId, FuncId, FuncOrDataId, - Linkage as ClifLinkage, Module as ClifModule, + Linkage as ClifLinkage, Module as ClifModule, ModuleError, }; use cranelift_object::{ObjectBackend, ObjectBuilder}; use cranelift_wasm::{translate_module, FuncTranslator, ModuleTranslationState, WasmError}; +use gimli::write::EhFrame; +use gimli::write::{Address, FrameTable}; use lucet_module::bindings::Bindings; use lucet_module::{ ModuleData, ModuleFeatures, SerializedModule, VersionInfo, LUCET_MODULE_SYM, MODULE_DATA_SYM, @@ -228,7 +233,7 @@ impl<'a> Compiler<'a> { _ => (cranelift_module::default_libcall_names())(libcall), }); - let mut builder = ObjectBuilder::new(isa, "lucet_guest".to_owned(), libcalls); + let mut builder = ObjectBuilder::new(isa, "lucet_guest".to_owned(), libcalls)?; builder.function_alignment(16); let mut clif_module: ClifModule = ClifModule::new(builder); @@ -268,7 +273,20 @@ impl<'a> Compiler<'a> { } pub fn object_file(mut self) -> Result { + let isa = Self::target_isa( + self.target.clone(), + self.opt_level, + &self.cpu_features, + self.canonicalize_nans, + )?; + let target_binary_format = isa.triple().binary_format; + let mut frame_table = FrameTable::default(); let mut func_translator = FuncTranslator::new(); + let cie_id = frame_table.add_cie( + isa.as_ref() + .create_systemv_cie() + .expect("creating a SystemV CIE does not fail"), + ); let mut function_manifest_ctx = ClifDataContext::new(); let mut function_manifest_bytes = Cursor::new(Vec::new()); let mut function_map: HashMap = HashMap::new(); @@ -278,6 +296,7 @@ impl<'a> Compiler<'a> { let mut clif_context = ClifContext::new(); clif_context.func.name = func.name.as_externalname(); clif_context.func.signature = func.signature.clone(); + clif_context.func.collect_debug_info(); func_translator .translate( @@ -301,6 +320,48 @@ impl<'a> Compiler<'a> { source, })?; + let unwind_info = clif_context + .create_unwind_info(isa.as_ref()) + .map_err(|err| Error::ClifModuleError(ModuleError::Compilation(err)))?; + if let Some(unwind_info) = unwind_info { + match target_binary_format { + target_lexicon::BinaryFormat::Elf | target_lexicon::BinaryFormat::Macho => { + if let UnwindInfo::SystemV(info) = unwind_info { + frame_table.add_fde( + cie_id, + info.to_fde(Address::Symbol { + symbol: func_id.as_u32() as usize, + addend: 0, + }), + ); + } else { + return Err(Error::ClifModuleError(ModuleError::Compilation( + CodegenError::Unsupported( + "Non-SystemV unwind information in ELF/MachO output." + .to_string(), + ), + ))); + } + } + objfmt => { + return Err(Error::ClifModuleError(ModuleError::Compilation( + CodegenError::Unsupported(format!( + "lucetc does not yet support consolidating \ + {:?} unwind information.", + objfmt + )), + ))); + } + } + } else { + return Err(Error::ClifModuleError(ModuleError::Compilation( + CodegenError::Unsupported(format!( + "lucetc does not yet support consolidated unwind \ + information where some functions have no unwind information." + )), + ))); + } + let size = compiled.size; let trap_data_id = traps.write(&mut self.clif_module, func.name.symbol())?; @@ -437,6 +498,43 @@ impl<'a> Compiler<'a> { self.clif_module .define_data(native_data_id, &native_data_ctx)?; + let mut eh_frame_ctx = cranelift_module::DataContext::new(); + + let mut eh_frame = EhFrame(EhFrameSink { + data: Vec::new(), + data_context: &mut eh_frame_ctx, + }); + frame_table + .write_eh_frame(&mut eh_frame) + .map_err(|e| cranelift_module::ModuleError::Backend(anyhow::anyhow!(e)))?; + let eh_frame_bytes = eh_frame.0.data; + + let eh_section_name = match target_binary_format { + target_lexicon::BinaryFormat::Elf => ".eh_frame", + target_lexicon::BinaryFormat::Macho => "__eh_frame", + objfmt => { + return Err(Error::ClifModuleError(ModuleError::Compilation( + CodegenError::Unsupported(format!( + "lucetc does not yet support consolidating \ + {:?} unwind information.", + objfmt + )), + ))); + } + }; + + eh_frame_ctx.set_segment_section("", eh_section_name); + eh_frame_ctx.define(eh_frame_bytes.into_boxed_slice()); + + let dataid = self.clif_module.declare_data( + eh_section_name, + ClifLinkage::Local, + false, + false, + None, + )?; + self.clif_module.define_data(dataid, &eh_frame_ctx)?; + let obj = ObjectFile::new(self.clif_module.finish())?; Ok(obj) diff --git a/lucetc/src/lib.rs b/lucetc/src/lib.rs index d19c56bca..9551a2e90 100644 --- a/lucetc/src/lib.rs +++ b/lucetc/src/lib.rs @@ -17,6 +17,7 @@ mod stack_probe; mod table; mod traps; mod types; +mod unwind; use crate::load::read_bytes; pub use crate::{ @@ -348,6 +349,8 @@ fn link_so( let mut ld_iter = env_ld.split_whitespace(); let ld_prog = ld_iter.next().expect("LD must not be empty"); let mut cmd_ld = Command::new(ld_prog); + // TODO: sufficiently crossplatform? + cmd_ld.arg("--eh-frame-hdr"); for flag in ld_iter { cmd_ld.arg(flag); } diff --git a/lucetc/src/unwind.rs b/lucetc/src/unwind.rs new file mode 100644 index 000000000..f05f9d238 --- /dev/null +++ b/lucetc/src/unwind.rs @@ -0,0 +1,46 @@ +use cranelift_module::{DataContext, FuncId}; +use gimli::write::{Address, Error, Result, Writer}; + +pub(crate) struct EhFrameSink<'a> { + pub data: Vec, + pub data_context: &'a mut DataContext, +} + +impl<'a> Writer for EhFrameSink<'a> { + type Endian = gimli::LittleEndian; + + fn endian(&self) -> Self::Endian { + gimli::LittleEndian + } + fn len(&self) -> usize { + self.data.len() + } + fn write(&mut self, bytes: &[u8]) -> Result<()> { + self.data.extend_from_slice(bytes); + Ok(()) + } + fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> { + if offset + bytes.len() > self.data.len() { + return Err(Error::LengthOutOfBounds); + } + self.data[offset..][..bytes.len()].copy_from_slice(bytes); + Ok(()) + } + + fn write_address(&mut self, address: Address, size: u8) -> Result<()> { + match address { + Address::Constant(val) => self.write_udata(val, size), + Address::Symbol { symbol, addend } => { + assert_eq!(addend, 0); + + let name = FuncId::from_u32(symbol as u32).into(); + let funcref = self.data_context.import_function(name); + let offset = self.data.len(); + self.data_context + .write_function_addr(offset as u32, funcref); + + self.write_udata(0, size) + } + } + } +} diff --git a/rust-toolchain b/rust-toolchain index 3987c4729..4ce925c82 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.43.1 +nightly-2020-06-06 diff --git a/wasmtime b/wasmtime index 3de418630..60ac091af 160000 --- a/wasmtime +++ b/wasmtime @@ -1 +1 @@ -Subproject commit 3de418630a263ca214931d69f796879be50d4f72 +Subproject commit 60ac091afe8df48f843ce8ad06a2bd1a626f6183