From 821c32c5609d0f951f4e856fc66e2b53cc1a2594 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Wed, 11 Mar 2020 18:24:08 -0700 Subject: [PATCH 1/5] Increase the size of the sigaltstack. Rust's stack overflow handler installs a sigaltstack stack with size SIGSTKSZ, which is too small for some of the things we do in signal handlers, and as of this writing lacks a guard page. Install bigger sigaltstack stacks so that we have enough space, and have a guard page. --- crates/api/src/trap.rs | 3 + crates/runtime/src/traphandlers.rs | 149 +++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/crates/api/src/trap.rs b/crates/api/src/trap.rs index 8b5d6f7b984d..6776c98f2c51 100644 --- a/crates/api/src/trap.rs +++ b/crates/api/src/trap.rs @@ -50,6 +50,9 @@ impl Trap { wasmtime_runtime::Trap::Wasm { desc, backtrace } => { Trap::new_with_trace(desc.to_string(), backtrace) } + wasmtime_runtime::Trap::OOM { backtrace } => { + Trap::new_with_trace("Out of memory".to_string(), backtrace) + } } } diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 7182a6506d66..adf465904b78 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -87,6 +87,19 @@ pub unsafe fn raise_lib_trap(trap: Trap) -> ! { tls::with(|info| info.unwrap().unwind_with(UnwindReason::LibTrap(trap))) } +/// Raises an out-of-memory trap immediately. +/// +/// # Safety +/// +/// Only safe to call when wasm code is on the stack, aka `wasmtime_call` or +/// `wasmtime_call_trampoline` must have been previously called. +pub unsafe fn raise_oom_trap() -> ! { + tls::with(|info| { + info.unwrap() + .unwind_with(UnwindReason::LibTrap(Trap::oom())) + }) +} + /// Carries a Rust panic across wasm code and resumes the panic on the other /// side. /// @@ -127,6 +140,12 @@ pub enum Trap { /// Native stack backtrace at the time the trap occurred backtrace: Backtrace, }, + + /// A trap indicating that the runtime was unable to allocate sufficient memory. + OOM { + /// Native stack backtrace at the time the OOM occurred + backtrace: Backtrace, + }, } impl fmt::Display for Trap { @@ -134,6 +153,7 @@ impl fmt::Display for Trap { match self { Trap::User(user) => user.fmt(f), Trap::Wasm { desc, .. } => desc.fmt(f), + Trap::OOM { .. } => write!(f, "Out of memory"), } } } @@ -152,6 +172,14 @@ impl Trap { let backtrace = Backtrace::new(); Trap::Wasm { desc, backtrace } } + + /// Construct a new OOM trap with the given source location and trap code. + /// + /// Internally saves a backtrace when constructed. + pub fn oom() -> Self { + let backtrace = Backtrace::new(); + Trap::OOM { backtrace } + } } /// Call the wasm function pointed to by `callee`. @@ -191,6 +219,10 @@ where F: FnMut(), { return CallThreadState::new(vmctx).with(|cx| { + // Ensure that we have our sigaltstack installed. + #[cfg(unix)] + unix_sigaltstack::TLS.with(|_| ()); + RegisterSetjmp( cx.jmp_buf.as_ptr(), call_closure::, @@ -397,3 +429,120 @@ mod tls { }) } } + +/// A module for registering a custom alternate signal stack (sigaltstack). +/// +/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not always +/// large enough for our signal handling code, and which as of this writing does not +/// have a guard page. Override it by creating and registering our own alternate stack +/// that is large enough and has a guard page. +#[cfg(unix)] +mod unix_sigaltstack { + use std::cell::RefCell; + use std::convert::TryInto; + use std::ptr::null_mut; + + thread_local! { + /// Thread-local state is lazy-initialized on the first time it's used, and dropped + /// when the thread exits. + pub(crate) static TLS: RefCell = RefCell::new(unsafe { Tls::new() }); + } + + /// The size of the sigaltstack (not including the guard, which will be added). + /// Make this large enough to run our signal handlers. + static STACK_SIZE: usize = libc::SIGSTKSZ * 4; + + /// A utility to register a new sigaltstack. + pub(crate) struct Tls { + guard_size: usize, + sigaltstack_alloc_size: usize, + new_stack: libc::stack_t, + } + + impl Tls { + unsafe fn new() -> Self { + let page_size: usize = libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap(); + let guard_size = page_size; + + let mut me = Self { + guard_size, + sigaltstack_alloc_size: guard_size + STACK_SIZE, + new_stack: libc::stack_t { + ss_sp: null_mut(), + ss_flags: 0, + ss_size: 0, + }, + }; + + // Allocate memory. + let ptr = libc::mmap( + null_mut(), + me.sigaltstack_alloc_size, + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ); + if ptr == libc::MAP_FAILED { + super::raise_oom_trap() + } + + // Prepare the stack, register it, and sanity check the old stack. + let stack_ptr = (ptr as usize + me.guard_size) as *mut libc::c_void; + me.new_stack = libc::stack_t { + ss_sp: stack_ptr, + ss_flags: 0, + ss_size: STACK_SIZE, + }; + let r = libc::mprotect(stack_ptr, STACK_SIZE, libc::PROT_READ | libc::PROT_WRITE); + assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed"); + + // Register the new alternate stack (and record the old one, so we can sanity-check it). + let mut old_stack = libc::stack_t { + ss_sp: null_mut(), + ss_flags: 0, + ss_size: 0, + }; + let r = libc::sigaltstack(&mut me.new_stack, &mut old_stack); + assert_eq!(r, 0, "registering new sigaltstack failed"); + + // Sanity-check the old stack. + assert_eq!( + old_stack.ss_flags, 0, + "old sigaltstack had unexpected flags set" + ); + assert!( + old_stack.ss_size <= STACK_SIZE, + "old sigaltstack had a larger stack" + ); + + me + } + } + + impl Drop for Tls { + fn drop(&mut self) { + unsafe { + // We expect Rust's libstd to have unregistered our sigaltstack by this point. + // Confirm that it has. + let mut old_stack = libc::stack_t { + ss_sp: null_mut(), + ss_flags: 0, + ss_size: 0, + }; + let r = libc::sigaltstack(null_mut(), &mut old_stack); + assert_eq!(r, 0, "querying for our sigaltstack failed"); + assert_ne!( + old_stack.ss_sp, self.new_stack.ss_sp, + "Rust's runtime didn't unregister our sigaltstack" + ); + + // Deallocate the stack memory. + let alloc_ptr = + (self.new_stack.ss_sp as usize - self.guard_size) as *mut libc::c_void; + let r = libc::munmap(alloc_ptr, self.sigaltstack_alloc_size); + assert_eq!(r, 0, "munmap failed during thread shutdown"); + } + } + } +} From cb1c12dfb57debe36a87604138c4487a9db9338e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 3 Apr 2020 11:50:55 -0700 Subject: [PATCH 2/5] Reorganize sigaltstack code a bit --- crates/api/src/trap.rs | 2 +- crates/runtime/src/traphandlers.rs | 190 ++++++++++++----------------- 2 files changed, 82 insertions(+), 110 deletions(-) diff --git a/crates/api/src/trap.rs b/crates/api/src/trap.rs index 6776c98f2c51..4b299dbc895b 100644 --- a/crates/api/src/trap.rs +++ b/crates/api/src/trap.rs @@ -51,7 +51,7 @@ impl Trap { Trap::new_with_trace(desc.to_string(), backtrace) } wasmtime_runtime::Trap::OOM { backtrace } => { - Trap::new_with_trace("Out of memory".to_string(), backtrace) + Trap::new_with_trace("out of memory".to_string(), backtrace) } } } diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 1633b6a5bf41..be314f10f5f5 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -296,19 +296,6 @@ pub unsafe fn raise_lib_trap(trap: Trap) -> ! { tls::with(|info| info.unwrap().unwind_with(UnwindReason::LibTrap(trap))) } -/// Raises an out-of-memory trap immediately. -/// -/// # Safety -/// -/// Only safe to call when wasm code is on the stack, aka `wasmtime_call` or -/// `wasmtime_call_trampoline` must have been previously called. -pub unsafe fn raise_oom_trap() -> ! { - tls::with(|info| { - info.unwrap() - .unwind_with(UnwindReason::LibTrap(Trap::oom())) - }) -} - /// Carries a Rust panic across wasm code and resumes the panic on the other /// side. /// @@ -400,11 +387,11 @@ pub unsafe fn catch_traps(vmctx: *mut VMContext, mut closure: F) -> Result<() where F: FnMut(), { - return CallThreadState::new(vmctx).with(|cx| { - // Ensure that we have our sigaltstack installed. - #[cfg(unix)] - unix_sigaltstack::TLS.with(|_| ()); + // Ensure that we have our sigaltstack installed. + #[cfg(unix)] + setup_unix_sigaltstack()?; + return CallThreadState::new(vmctx).with(|cx| { RegisterSetjmp( cx.jmp_buf.as_ptr(), call_closure::, @@ -628,116 +615,101 @@ mod tls { /// A module for registering a custom alternate signal stack (sigaltstack). /// -/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not always -/// large enough for our signal handling code, and which as of this writing does not -/// have a guard page. Override it by creating and registering our own alternate stack -/// that is large enough and has a guard page. +/// Rust's libstd installs an alternate stack with size `SIGSTKSZ`, which is not +/// always large enough for our signal handling code. Override it by creating +/// and registering our own alternate stack that is large enough and has a guard +/// page. #[cfg(unix)] -mod unix_sigaltstack { +fn setup_unix_sigaltstack() -> Result<(), Trap> { use std::cell::RefCell; use std::convert::TryInto; use std::ptr::null_mut; thread_local! { - /// Thread-local state is lazy-initialized on the first time it's used, and dropped - /// when the thread exits. - pub(crate) static TLS: RefCell = RefCell::new(unsafe { Tls::new() }); + /// Thread-local state is lazy-initialized on the first time it's used, + /// and dropped when the thread exits. + static TLS: RefCell = RefCell::new(Tls::None); } - /// The size of the sigaltstack (not including the guard, which will be added). - /// Make this large enough to run our signal handlers. - static STACK_SIZE: usize = libc::SIGSTKSZ * 4; - - /// A utility to register a new sigaltstack. - pub(crate) struct Tls { - guard_size: usize, - sigaltstack_alloc_size: usize, - new_stack: libc::stack_t, + /// The size of the sigaltstack (not including the guard, which will be + /// added). Make this large enough to run our signal handlers. + const MIN_STACK_SIZE: usize = 16 * 4096; + + enum Tls { + None, + Allocated { + mmap_ptr: *mut libc::c_void, + mmap_size: usize, + }, + BigEnough, } - impl Tls { - unsafe fn new() -> Self { - let page_size: usize = libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap(); - let guard_size = page_size; - - let mut me = Self { - guard_size, - sigaltstack_alloc_size: guard_size + STACK_SIZE, - new_stack: libc::stack_t { - ss_sp: null_mut(), - ss_flags: 0, - ss_size: 0, - }, - }; + return TLS.with(|slot| unsafe { + let mut slot = slot.borrow_mut(); + match *slot { + Tls::None => {} + // already checked + _ => return Ok(()), + } - // Allocate memory. - let ptr = libc::mmap( - null_mut(), - me.sigaltstack_alloc_size, - libc::PROT_NONE, - libc::MAP_PRIVATE | libc::MAP_ANON, - -1, - 0, - ); - if ptr == libc::MAP_FAILED { - super::raise_oom_trap() - } + // Check to see if the existing sigaltstack, if it exists, is big + // enough. If so we don't need to allocate our own. + let mut old_stack = mem::zeroed(); + let r = libc::sigaltstack(ptr::null(), &mut old_stack); + assert_eq!(r, 0, "learning about sigaltstack failed"); + if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE { + *slot = Tls::BigEnough; + return Ok(()) + } - // Prepare the stack, register it, and sanity check the old stack. - let stack_ptr = (ptr as usize + me.guard_size) as *mut libc::c_void; - me.new_stack = libc::stack_t { - ss_sp: stack_ptr, - ss_flags: 0, - ss_size: STACK_SIZE, - }; - let r = libc::mprotect(stack_ptr, STACK_SIZE, libc::PROT_READ | libc::PROT_WRITE); - assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed"); - - // Register the new alternate stack (and record the old one, so we can sanity-check it). - let mut old_stack = libc::stack_t { - ss_sp: null_mut(), - ss_flags: 0, - ss_size: 0, - }; - let r = libc::sigaltstack(&mut me.new_stack, &mut old_stack); - assert_eq!(r, 0, "registering new sigaltstack failed"); - - // Sanity-check the old stack. - assert_eq!( - old_stack.ss_flags, 0, - "old sigaltstack had unexpected flags set" - ); - assert!( - old_stack.ss_size <= STACK_SIZE, - "old sigaltstack had a larger stack" - ); - - me + // ... but failing that we need to allocate our own, so do all that + // here. + let page_size: usize = libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap(); + let guard_size = page_size; + let alloc_size = guard_size + MIN_STACK_SIZE; + + let ptr = libc::mmap( + null_mut(), + alloc_size, + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ); + if ptr == libc::MAP_FAILED { + return Err(Trap::oom()) } - } + + // Prepare the stack with readable/writable memory and then reigster it + // with `sigaltstack`. + let stack_ptr = (ptr as usize + guard_size) as *mut libc::c_void; + let r = libc::mprotect(stack_ptr, MIN_STACK_SIZE, libc::PROT_READ | libc::PROT_WRITE); + assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed"); + let new_stack = libc::stack_t { + ss_sp: stack_ptr, + ss_flags: 0, + ss_size: MIN_STACK_SIZE, + }; + let r = libc::sigaltstack(&new_stack, ptr::null_mut()); + assert_eq!(r, 0, "registering new sigaltstack failed"); + + *slot = Tls::Allocated { + mmap_ptr: ptr, + mmap_size: alloc_size, + }; + Ok(()) + }); impl Drop for Tls { fn drop(&mut self) { + let (ptr, size) = match self { + Tls::Allocated { mmap_ptr, mmap_size } => (*mmap_ptr, *mmap_size), + _ => return, + }; unsafe { - // We expect Rust's libstd to have unregistered our sigaltstack by this point. - // Confirm that it has. - let mut old_stack = libc::stack_t { - ss_sp: null_mut(), - ss_flags: 0, - ss_size: 0, - }; - let r = libc::sigaltstack(null_mut(), &mut old_stack); - assert_eq!(r, 0, "querying for our sigaltstack failed"); - assert_ne!( - old_stack.ss_sp, self.new_stack.ss_sp, - "Rust's runtime didn't unregister our sigaltstack" - ); - // Deallocate the stack memory. - let alloc_ptr = - (self.new_stack.ss_sp as usize - self.guard_size) as *mut libc::c_void; - let r = libc::munmap(alloc_ptr, self.sigaltstack_alloc_size); - assert_eq!(r, 0, "munmap failed during thread shutdown"); + let r = libc::munmap(ptr, size); + debug_assert_eq!(r, 0, "munmap failed during thread shutdown"); } } } From 47f98c10c6b8eb589a61bb0e20872f2b5cb8c9a3 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 3 Apr 2020 11:52:27 -0700 Subject: [PATCH 3/5] rustfmt --- crates/runtime/src/traphandlers.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index be314f10f5f5..548c88ce9d26 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -659,7 +659,7 @@ fn setup_unix_sigaltstack() -> Result<(), Trap> { assert_eq!(r, 0, "learning about sigaltstack failed"); if old_stack.ss_flags & libc::SS_DISABLE == 0 && old_stack.ss_size >= MIN_STACK_SIZE { *slot = Tls::BigEnough; - return Ok(()) + return Ok(()); } // ... but failing that we need to allocate our own, so do all that @@ -677,13 +677,17 @@ fn setup_unix_sigaltstack() -> Result<(), Trap> { 0, ); if ptr == libc::MAP_FAILED { - return Err(Trap::oom()) + return Err(Trap::oom()); } // Prepare the stack with readable/writable memory and then reigster it // with `sigaltstack`. let stack_ptr = (ptr as usize + guard_size) as *mut libc::c_void; - let r = libc::mprotect(stack_ptr, MIN_STACK_SIZE, libc::PROT_READ | libc::PROT_WRITE); + let r = libc::mprotect( + stack_ptr, + MIN_STACK_SIZE, + libc::PROT_READ | libc::PROT_WRITE, + ); assert_eq!(r, 0, "mprotect to configure memory for sigaltstack failed"); let new_stack = libc::stack_t { ss_sp: stack_ptr, @@ -703,7 +707,10 @@ fn setup_unix_sigaltstack() -> Result<(), Trap> { impl Drop for Tls { fn drop(&mut self) { let (ptr, size) = match self { - Tls::Allocated { mmap_ptr, mmap_size } => (*mmap_ptr, *mmap_size), + Tls::Allocated { + mmap_ptr, + mmap_size, + } => (*mmap_ptr, *mmap_size), _ => return, }; unsafe { From 56dd0d1b52b0c0c400af2d11606177b1ffdfd83d Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 3 Apr 2020 12:05:25 -0700 Subject: [PATCH 4/5] Don't pre-init backtrace things --- crates/runtime/src/traphandlers.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 548c88ce9d26..ed3704ca3043 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -252,16 +252,6 @@ pub fn init() { } fn real_init() { - // This is a really weird and unfortunate function call. For all the gory - // details see #829, but the tl;dr; is that in a trap handler we have 2 - // pages of stack space on Linux, and calling into libunwind which triggers - // the dynamic loader blows the stack. - // - // This is a dumb hack to work around this system-specific issue by - // capturing a backtrace once in the lifetime of a process to ensure that - // when we capture a backtrace in the trap handler all caches are primed, - // aka the dynamic loader has resolved all the relevant symbols. - drop(backtrace::Backtrace::new_unresolved()); unsafe { platform_init(); } From 906843306e3c674699cb0e6ec8b1f7e6349661ee Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 3 Apr 2020 14:36:00 -0700 Subject: [PATCH 5/5] typo --- crates/runtime/src/traphandlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index ed3704ca3043..657abb62212d 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -670,7 +670,7 @@ fn setup_unix_sigaltstack() -> Result<(), Trap> { return Err(Trap::oom()); } - // Prepare the stack with readable/writable memory and then reigster it + // Prepare the stack with readable/writable memory and then register it // with `sigaltstack`. let stack_ptr = (ptr as usize + guard_size) as *mut libc::c_void; let r = libc::mprotect(