diff --git a/.github/workflows/mac-ci.yml.disabled b/.github/workflows/mac-ci.yml similarity index 69% rename from .github/workflows/mac-ci.yml.disabled rename to .github/workflows/mac-ci.yml index 29ccb688d..a9ebfc62d 100644 --- a/.github/workflows/mac-ci.yml.disabled +++ b/.github/workflows/mac-ci.yml @@ -18,9 +18,10 @@ jobs: - name: Install wasi-sdk (macos) run: | - curl -sS -L -O https://github.com/CraneStation/wasi-sdk/releases/download/wasi-sdk-7/wasi-sdk-7.0-macos.tar.gz - tar xf wasi-sdk-7.0-macos.tar.gz - sudo mv wasi-sdk-7.0/opt /opt + curl -sS -L -O https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-8/wasi-sdk-8.0-macos.tar.gz + tar xf wasi-sdk-8.0-macos.tar.gz + sudo mkdir -p /opt/wasi-sdk + sudo mv wasi-sdk-8.0/* /opt/wasi-sdk/ - name: Test Lucet run: make test-except-fuzz diff --git a/lucet-runtime/lucet-runtime-internals/src/context/mod.rs b/lucet-runtime/lucet-runtime-internals/src/context/mod.rs index 859b1a20b..a6f3b13b3 100644 --- a/lucet-runtime/lucet-runtime-internals/src/context/mod.rs +++ b/lucet-runtime/lucet-runtime-internals/src/context/mod.rs @@ -6,7 +6,6 @@ mod tests; use crate::instance::Instance; use crate::val::{val_to_reg, val_to_stack, RegVal, UntypedRetVal, Val}; -use nix::sys::signal; use std::arch::x86_64::{__m128, _mm_setzero_ps}; use std::ptr::NonNull; use std::{mem, ptr}; @@ -123,7 +122,6 @@ pub struct Context { // TODO ACF 2019-10-23: make Instance into a generic parameter? backstop_callback: *const unsafe extern "C" fn(*mut Instance), callback_data: *mut Instance, - sigset: signal::SigSet, } impl Context { @@ -137,7 +135,6 @@ impl Context { parent_ctx: ptr::null_mut(), backstop_callback: Context::default_backstop_callback as *const _, callback_data: ptr::null_mut(), - sigset: signal::SigSet::empty(), } } @@ -468,23 +465,16 @@ impl Context { let (stack, stack_start) = stack_builder.into_inner(); - // RSP, RBP, and sigset still remain to be initialized. // Stack pointer: this points to the return address that will be used by `swap`, in place // of the original (eg, in the host) return address. The return address this points to is // the address of the first function to run on `swap`: `lucet_context_bootstrap`. child.gpr.rsp = &mut stack[stack.len() - stack_start] as *mut u64 as u64; + // Base pointer: `rbp` will be saved through all guest code, and preserved for when we + // reach the backstop. This allows us to prepare an argument for `lucet_context_backstop` + // even at the entrypoint of the guest. child.gpr.rbp = child as *const Context as u64; - // Read the mask to be restored if we ever need to jump out of a signal handler. If this - // isn't possible, die. - signal::pthread_sigmask( - signal::SigmaskHow::SIG_SETMASK, - None, - Some(&mut child.sigset), - ) - .expect("pthread_sigmask could not be retrieved"); - Ok(()) } @@ -629,16 +619,6 @@ impl Context { lucet_context_set(to as *const Context); } - /// Like `set`, but also manages the return from a signal handler. - /// - /// TODO: the return type of this function should really be `Result`, but using - /// `!` as a type like that is currently experimental. - #[inline] - pub unsafe fn set_from_signal(to: &Context) -> Result<(), nix::Error> { - signal::pthread_sigmask(signal::SigmaskHow::SIG_SETMASK, Some(&to.sigset), None)?; - Context::set(to) - } - /// Clear (zero) return values. pub fn clear_retvals(&mut self) { self.retvals_gp = [0; 2]; @@ -726,7 +706,7 @@ extern "C" { /// Performs the context switch; implemented in assembly. /// /// Never returns because the current context is discarded. - fn lucet_context_set(to: *const Context) -> !; + pub(crate) fn lucet_context_set(to: *const Context) -> !; /// Runs an entry callback after performing a context switch. Implemented in assembly. /// diff --git a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs index b638db30d..f417b00cb 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs @@ -1,5 +1,4 @@ use crate::alloc::validate_sigstack_size; -use crate::context::Context; use crate::error::Error; use crate::instance::{ siginfo_ext::SiginfoExt, FaultDetails, Instance, State, TerminationDetails, CURRENT_INSTANCE, @@ -333,11 +332,23 @@ extern "C" fn handle_signal(signum: c_int, siginfo_ptr: *mut siginfo_t, ucontext }); if switch_to_host { - HOST_CTX.with(|host_ctx| unsafe { - Context::set_from_signal(&*host_ctx.get()) - .expect("can successfully switch back to the host context"); - }); - unreachable!() + // Switch to host by preparing the context to switch when we return from the signal andler. + // We must return from the signal handler for POSIX reasons, so instead prepare the context + // that the signal handler will resume the program as if a call were made. First, by + // pointing the instruction pointer at `lucet_context_set`, then by preparing the argument + // that `lucet_context_set` should read from `rdi` - the context to switch to. + // + // NOTE: it is absolutely critical that `lucet_context_set` does not use the guest stack! + // If it did, and the signal being handled were a segfault from reaching the guard page, + // there would be no stack available for the function we return to. By not using stack + // space, `lucet_context_set` is safe for use even in handling guard page faults. + // + // TODO: `rdi` is only correct for SysV (unixy) calling conventions! For Windows x86_64 this + // would be `rcx`, with other architectures being their own question. + ctx.set_ip(crate::context::lucet_context_set as *const c_void); + HOST_CTX.with(|host_ctx| { + ctx.set_rdi(host_ctx.get() as u64); + }) } } diff --git a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs index dd5418480..24be83113 100644 --- a/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs +++ b/lucet-runtime/lucet-runtime-internals/src/region/mmap.rs @@ -376,10 +376,17 @@ impl MmapRegion { // lay out the other sections in memory let heap = mem as usize + instance_heap_offset(); - let stack = heap + region.limits.heap_address_space_size + host_page_size(); + let stack_guard = heap + region.limits.heap_address_space_size; + let stack = stack_guard + host_page_size(); let globals = stack + region.limits.stack_size; let sigstack = globals + region.limits.globals_size + host_page_size(); + // ensure we've accounted for all space + assert_eq!( + sigstack + region.limits.signal_stack_size - mem as usize, + region.limits.total_memory_size() + ); + Ok(Slot { start: mem, heap: heap as *mut c_void, diff --git a/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs b/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs index fb576d8a1..da58e43cb 100644 --- a/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs +++ b/lucet-runtime/lucet-runtime-internals/src/sysdeps/linux.rs @@ -1,50 +1,56 @@ -use libc::{c_void, ucontext_t, REG_RIP}; +use libc::{c_void, ucontext_t, REG_RDI, REG_RIP}; #[derive(Clone, Copy, Debug)] -pub struct UContextPtr(*const ucontext_t); +pub struct UContextPtr(*mut ucontext_t); impl UContextPtr { #[inline] - pub fn new(ptr: *const c_void) -> Self { + pub fn new(ptr: *mut c_void) -> Self { assert!(!ptr.is_null(), "non-null context"); - UContextPtr(ptr as *const ucontext_t) + UContextPtr(ptr as *mut ucontext_t) } #[inline] pub fn get_ip(self) -> *const c_void { - let mcontext = &unsafe { *(self.0) }.uc_mcontext; + let mcontext = &unsafe { self.0.as_ref().unwrap() }.uc_mcontext; mcontext.gregs[REG_RIP as usize] as *const _ } + + #[inline] + pub fn set_ip(self, new_ip: *const c_void) { + let mut mcontext = &mut unsafe { self.0.as_mut().unwrap() }.uc_mcontext; + mcontext.gregs[REG_RIP as usize] = new_ip as i64; + } + + #[inline] + pub fn set_rdi(self, new_rdi: u64) { + let mut mcontext = &mut unsafe { self.0.as_mut().unwrap() }.uc_mcontext; + mcontext.gregs[REG_RDI as usize] = new_rdi as i64; + } } #[repr(C)] #[derive(Clone, Copy)] pub struct UContext { - context: ucontext_t, + context: *mut ucontext_t, } impl UContext { #[inline] - pub fn new(ptr: *const c_void) -> Self { + pub fn new(ptr: *mut c_void) -> Self { UContext { - context: *unsafe { - (ptr as *const ucontext_t) - .as_ref() - .expect("non-null context") - }, + context: unsafe { (ptr as *mut ucontext_t).as_mut().expect("non-null context") }, } } pub fn as_ptr(&mut self) -> UContextPtr { - UContextPtr::new(&self.context as *const _ as *const _) + UContextPtr::new(self.context as *mut _ as *mut _) } } impl Into for UContextPtr { #[inline] fn into(self) -> UContext { - UContext { - context: unsafe { *(self.0) }, - } + UContext { context: self.0 } } } diff --git a/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs b/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs index 3ea2e1c86..ebcae4cdf 100644 --- a/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs +++ b/lucet-runtime/lucet-runtime-internals/src/sysdeps/macos.rs @@ -118,54 +118,59 @@ pub struct ucontext_t { pub uc_stack: sigaltstack, pub uc_link: *const ucontext_t, pub uc_mcsize: size_t, - pub uc_mcontext: *const mcontext64, + pub uc_mcontext: *mut mcontext64, } #[derive(Clone, Copy, Debug)] -pub struct UContextPtr(*const ucontext_t); +pub struct UContextPtr(*mut ucontext_t); impl UContextPtr { #[inline] - pub fn new(ptr: *const c_void) -> Self { + pub fn new(ptr: *mut c_void) -> Self { assert!(!ptr.is_null(), "non-null context"); - UContextPtr(ptr as *const ucontext_t) + UContextPtr(ptr as *mut ucontext_t) } #[inline] pub fn get_ip(self) -> *const c_void { - let mcontext = &unsafe { *(*self.0).uc_mcontext }; + let mcontext = unsafe { (*self.0).uc_mcontext.as_ref().unwrap() }; mcontext.ss.rip as *const _ } + + #[inline] + pub fn set_ip(self, new_ip: *const c_void) { + let mcontext: &mut mcontext64 = unsafe { &mut (*self.0).uc_mcontext.as_mut().unwrap() }; + mcontext.ss.rip = new_ip as u64; + } + + #[inline] + pub fn set_rdi(self, new_rdi: u64) { + let mcontext: &mut mcontext64 = unsafe { &mut (*self.0).uc_mcontext.as_mut().unwrap() }; + mcontext.ss.rdi = new_rdi; + } } #[derive(Clone, Copy)] #[repr(C)] pub struct UContext { - context: ucontext_t, - mcontext: mcontext64, + context: *mut ucontext_t, } impl UContext { #[inline] - pub fn new(ptr: *const c_void) -> Self { - let context = *unsafe { - (ptr as *const ucontext_t) - .as_ref() - .expect("non-null context") - }; - let mcontext = unsafe { *context.uc_mcontext }; - UContext { context, mcontext } + pub fn new(ptr: *mut c_void) -> Self { + let context = unsafe { (ptr as *mut ucontext_t).as_mut().expect("non-null context") }; + UContext { context } } pub fn as_ptr(&mut self) -> UContextPtr { - self.context.uc_mcontext = &self.mcontext; - UContextPtr::new(&self.context as *const _ as *const _) + UContextPtr::new(self.context as *mut _ as *mut _) } } impl Into for UContextPtr { #[inline] fn into(self) -> UContext { - UContext::new(self.0 as *const _) + UContext::new(self.0 as *mut _) } } diff --git a/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs b/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs index f7b6f0693..dcab11e51 100644 --- a/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs +++ b/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs @@ -141,7 +141,7 @@ pub fn mock_traps_module() -> Arc { macro_rules! guest_fault_tests { ( $TestRegion:path ) => { use lazy_static::lazy_static; - use libc::{c_void, pthread_kill, pthread_self, siginfo_t, SIGALRM, SIGSEGV}; + use libc::{c_void, pthread_kill, pthread_self, siginfo_t, SIGALRM, SIGBUS, SIGSEGV}; use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; use lucet_runtime::{ lucet_hostcall, lucet_hostcall_terminate, lucet_internal_ensure_linked, DlModule, @@ -166,6 +166,16 @@ macro_rules! guest_fault_tests { static mut RECOVERABLE_PTR: *mut libc::c_char = ptr::null_mut(); + #[cfg(target_os = "linux")] + const INVALID_PERMISSION_FAULT: libc::c_int = SIGSEGV; + #[cfg(not(target_os = "linux"))] + const INVALID_PERMISSION_FAULT: libc::c_int = SIGBUS; + + #[cfg(target_os = "linux")] + const INVALID_PERMISSION_SIGNAL: Signal = Signal::SIGSEGV; + #[cfg(not(target_os = "linux"))] + const INVALID_PERMISSION_SIGNAL: Signal = Signal::SIGBUS; + unsafe fn recoverable_ptr_setup() { assert!(RECOVERABLE_PTR.is_null()); RECOVERABLE_PTR = mmap( @@ -441,8 +451,7 @@ macro_rules! guest_fault_tests { _siginfo_ptr: *const siginfo_t, _ucontext_ptr: *const c_void, ) -> SignalBehavior { - // Triggered by a SIGSEGV writing to protected page - assert!(signum == SIGSEGV); + assert!(signum == INVALID_PERMISSION_FAULT); // The fault was caused by writing to a protected page at `recoverable_ptr`. Make that // no longer be a fault @@ -488,8 +497,7 @@ macro_rules! guest_fault_tests { _siginfo_ptr: *const siginfo_t, _ucontext_ptr: *const c_void, ) -> SignalBehavior { - // Triggered by a SIGSEGV writing to protected page - assert!(signum == SIGSEGV); + assert!(signum == INVALID_PERMISSION_FAULT); // Terminate guest SignalBehavior::Terminate @@ -542,7 +550,7 @@ macro_rules! guest_fault_tests { #[test] fn sigsegv_handler_saved_restored() { lazy_static! { - static ref HOST_SIGSEGV_TRIGGERED: Mutex = Mutex::new(false); + static ref HOST_FAULT_TRIGGERED: Mutex = Mutex::new(false); } extern "C" fn host_sigsegv_handler( @@ -550,10 +558,9 @@ macro_rules! guest_fault_tests { _siginfo_ptr: *mut siginfo_t, _ucontext_ptr: *mut c_void, ) { - // Triggered by a SIGSEGV writing to protected page - assert!(signum == SIGSEGV); + assert!(signum == INVALID_PERMISSION_FAULT); unsafe { recoverable_ptr_make_accessible() }; - *HOST_SIGSEGV_TRIGGERED.lock().unwrap() = true; + *HOST_FAULT_TRIGGERED.lock().unwrap() = true; } test_ex(|| { // make sure only one test using RECOVERABLE_PTR is running at once @@ -570,7 +577,7 @@ macro_rules! guest_fault_tests { SaFlags::SA_RESTART, SigSet::all(), ); - unsafe { sigaction(Signal::SIGSEGV, &sa).expect("sigaction succeeds") }; + unsafe { sigaction(INVALID_PERMISSION_SIGNAL, &sa).expect("sigaction succeeds") }; match inst.run("illegal_instr", &[]) { Err(Error::RuntimeFault(details)) => { @@ -583,20 +590,20 @@ macro_rules! guest_fault_tests { unsafe { recoverable_ptr_setup(); } - *HOST_SIGSEGV_TRIGGERED.lock().unwrap() = false; + *HOST_FAULT_TRIGGERED.lock().unwrap() = false; // accessing this should trigger the segfault unsafe { *RECOVERABLE_PTR = 0; } - assert!(*HOST_SIGSEGV_TRIGGERED.lock().unwrap()); + assert!(*HOST_FAULT_TRIGGERED.lock().unwrap()); // clean up unsafe { recoverable_ptr_teardown(); sigaction( - Signal::SIGSEGV, + INVALID_PERMISSION_SIGNAL, &SigAction::new(SigHandler::SigDfl, SaFlags::SA_RESTART, SigSet::empty()), ) .expect("sigaction succeeds"); @@ -609,7 +616,7 @@ macro_rules! guest_fault_tests { #[test] fn sigsegv_handler_during_guest() { lazy_static! { - static ref HOST_SIGSEGV_TRIGGERED: Mutex = Mutex::new(false); + static ref HOST_FAULT_TRIGGERED: Mutex = Mutex::new(false); } extern "C" fn host_sigsegv_handler( @@ -617,10 +624,9 @@ macro_rules! guest_fault_tests { _siginfo_ptr: *mut siginfo_t, _ucontext_ptr: *mut c_void, ) { - // Triggered by a SIGSEGV writing to protected page - assert!(signum == SIGSEGV); + assert!(signum == INVALID_PERMISSION_FAULT); unsafe { recoverable_ptr_make_accessible() }; - *HOST_SIGSEGV_TRIGGERED.lock().unwrap() = true; + *HOST_FAULT_TRIGGERED.lock().unwrap() = true; } #[lucet_hostcall] @@ -638,8 +644,9 @@ macro_rules! guest_fault_tests { SigSet::empty(), ); - let saved_sa = - unsafe { sigaction(Signal::SIGSEGV, &sa).expect("sigaction succeeds") }; + let saved_fault_sa = unsafe { + sigaction(INVALID_PERMISSION_SIGNAL, &sa).expect("sigaction succeeds") + }; // The original thread will run `sleepy_guest`, and the new thread will dereference a null // pointer after a delay. This should lead to a sigsegv while the guest is running, @@ -665,14 +672,14 @@ macro_rules! guest_fault_tests { unsafe { recoverable_ptr_setup(); } - *HOST_SIGSEGV_TRIGGERED.lock().unwrap() = false; + *HOST_FAULT_TRIGGERED.lock().unwrap() = false; // accessing this should trigger the segfault unsafe { *RECOVERABLE_PTR = 0; } - assert!(*HOST_SIGSEGV_TRIGGERED.lock().unwrap()); + assert!(*HOST_FAULT_TRIGGERED.lock().unwrap()); child.join().expect("can join on child"); @@ -680,7 +687,8 @@ macro_rules! guest_fault_tests { unsafe { recoverable_ptr_teardown(); // sigaltstack(&saved_sigstack).expect("sigaltstack succeeds"); - sigaction(Signal::SIGSEGV, &saved_sa).expect("sigaction succeeds"); + sigaction(INVALID_PERMISSION_SIGNAL, &saved_fault_sa) + .expect("sigaction succeeds"); } drop(recoverable_ptr_lock); @@ -720,7 +728,7 @@ macro_rules! guest_fault_tests { ForkResult::Parent { child } => { match waitpid(Some(child), None).expect("can wait on child") { WaitStatus::Signaled(_, sig, _) => { - assert_eq!(sig, Signal::SIGSEGV); + assert_eq!(sig, INVALID_PERMISSION_SIGNAL); } ws => panic!("unexpected wait status: {:?}", ws), } diff --git a/lucet-runtime/tests/c_api.c b/lucet-runtime/tests/c_api.c index b799d186f..a2665bab2 100644 --- a/lucet-runtime/tests/c_api.c +++ b/lucet-runtime/tests/c_api.c @@ -11,7 +11,7 @@ bool lucet_runtime_test_expand_heap(struct lucet_dl_module *mod) .heap_address_space_size = 8 * 1024 * 1024, .stack_size = 64 * 1024, .globals_size = 4096, - .signal_stack_size = 12 * 1024, + .signal_stack_size = 32 * 1024, }; enum lucet_error err; @@ -98,7 +98,7 @@ bool lucet_runtime_test_yield_resume(struct lucet_dl_module *mod) .heap_address_space_size = 8 * 1024 * 1024, .stack_size = 64 * 1024, .globals_size = 4096, - .signal_stack_size = 12 * 1024, + .signal_stack_size = 32 * 1024, }; enum lucet_error err; diff --git a/lucet-runtime/tests/version_checks.rs b/lucet-runtime/tests/version_checks.rs index fa5c9a30c..56bc1684a 100644 --- a/lucet-runtime/tests/version_checks.rs +++ b/lucet-runtime/tests/version_checks.rs @@ -2,9 +2,14 @@ use lucet_runtime::{DlModule, Error}; #[test] pub fn reject_old_modules() { - let err = DlModule::load("./tests/version_checks/old_module.so") - .err() - .unwrap(); + // for platforms where modules are ELF (BSD, Linux, ...), exclude macos as it uses a different + // file format (MachO) + #[cfg(all(unix, not(target_os = "macos")))] + const MODULE_PATH: &'static str = "./tests/version_checks/old_module.so"; + #[cfg(target_os = "macos")] + const MODULE_PATH: &'static str = "./tests/version_checks/old_module.dylib"; + + let err = DlModule::load(MODULE_PATH).err().unwrap(); if let Error::ModuleError(e) = err { let msg = format!("{}", e); diff --git a/lucet-runtime/tests/version_checks/old_module.dylib b/lucet-runtime/tests/version_checks/old_module.dylib new file mode 100755 index 000000000..27743542c Binary files /dev/null and b/lucet-runtime/tests/version_checks/old_module.dylib differ