Implement interrupting wasm code, reimplement stack overflow (#1490)

* Implement interrupting wasm code, reimplement stack overflow

This commit is a relatively large change for wasmtime with two main
goals:

* Primarily this enables interrupting executing wasm code with a trap,
  preventing infinite loops in wasm code. Note that resumption of the
  wasm code is not a goal of this commit.

* Additionally this commit reimplements how we handle stack overflow to
  ensure that host functions always have a reasonable amount of stack to
  run on. This fixes an issue where we might longjmp out of a host
  function, skipping destructors.

Lots of various odds and ends end up falling out in this commit once the
two goals above were implemented. The strategy for implementing this was
also lifted from Spidermonkey and existing functionality inside of
Cranelift. I've tried to write up thorough documentation of how this all
works in `crates/environ/src/cranelift.rs` where gnarly-ish bits are.

A brief summary of how this works is that each function and each loop
header now checks to see if they're interrupted. Interrupts and the
stack overflow check are actually folded into one now, where function
headers check to see if they've run out of stack and the sentinel value
used to indicate an interrupt, checked in loop headers, tricks functions
into thinking they're out of stack. An interrupt is basically just
writing a value to a location which is read by JIT code.

When interrupts are delivered and what triggers them has been left up to
embedders of the `wasmtime` crate. The `wasmtime::Store` type has a
method to acquire an `InterruptHandle`, where `InterruptHandle` is a
`Send` and `Sync` type which can travel to other threads (or perhaps
even a signal handler) to get notified from. It's intended that this
provides a good degree of flexibility when interrupting wasm code. Note
though that this does have a large caveat where interrupts don't work
when you're interrupting host code, so if you've got a host import
blocking for a long time an interrupt won't actually be received until
the wasm starts running again.

Some fallout included from this change is:

* Unix signal handlers are no longer registered with `SA_ONSTACK`.
  Instead they run on the native stack the thread was already using.
  This is possible since stack overflow isn't handled by hitting the
  guard page, but rather it's explicitly checked for in wasm now. Native
  stack overflow will continue to abort the process as usual.

* Unix sigaltstack management is now no longer necessary since we don't
  use it any more.

* Windows no longer has any need to reset guard pages since we no longer
  try to recover from faults on guard pages.

* On all targets probestack intrinsics are disabled since we use a
  different mechanism for catching stack overflow.

* The C API has been updated with interrupts handles. An example has
  also been added which shows off how to interrupt a module.

Closes #139
Closes #860
Closes #900

* Update comment about magical interrupt value

* Store stack limit as a global value, not a closure

* Run rustfmt

* Handle review comments

* Add a comment about SA_ONSTACK

* Use `usize` for type of `INTERRUPTED`

* Parse human-readable durations

* Bring back sigaltstack handling

Allows libstd to print out stack overflow on failure still.

* Add parsing and emission of stack limit-via-preamble

* Fix new example for new apis

* Fix host segfault test in release mode

* Fix new doc example
This commit is contained in:
Alex Crichton
2020-04-21 13:03:28 -05:00
committed by GitHub
parent 4a63a4d86e
commit c9a0ba81a0
45 changed files with 1361 additions and 143 deletions

View File

@@ -176,6 +176,7 @@ macro_rules! getters {
// of the closure. Pass the export in so that we can call it.
let instance = self.instance.clone();
let export = self.export.clone();
let max_wasm_stack = self.store.engine().config().max_wasm_stack;
// ... and then once we've passed the typechecks we can hand out our
// object since our `transmute` below should be safe!
@@ -191,7 +192,7 @@ macro_rules! getters {
>(export.address);
let mut ret = None;
$(let $args = $args.into_abi();)*
wasmtime_runtime::catch_traps(export.vmctx, || {
wasmtime_runtime::catch_traps(export.vmctx, max_wasm_stack, || {
ret = Some(fnptr(export.vmctx, ptr::null_mut(), $($args,)*));
}).map_err(Trap::from_jit)?;
@@ -558,14 +559,18 @@ impl Func {
// Call the trampoline.
if let Err(error) = unsafe {
wasmtime_runtime::catch_traps(self.export.vmctx, || {
(self.trampoline)(
self.export.vmctx,
ptr::null_mut(),
self.export.address,
values_vec.as_mut_ptr(),
)
})
wasmtime_runtime::catch_traps(
self.export.vmctx,
self.store.engine().config().max_wasm_stack,
|| {
(self.trampoline)(
self.export.vmctx,
ptr::null_mut(),
self.export.address,
values_vec.as_mut_ptr(),
)
},
)
} {
return Err(Trap::from_jit(error).into());
}

View File

@@ -35,6 +35,7 @@ fn instantiate(
&mut resolver,
sig_registry,
config.memory_creator.as_ref().map(|a| a as _),
config.max_wasm_stack,
host,
)
.map_err(|e| -> Error {

View File

@@ -1,6 +1,6 @@
use crate::externals::MemoryCreator;
use crate::trampoline::MemoryCreatorProxy;
use anyhow::Result;
use anyhow::{bail, Result};
use std::cell::RefCell;
use std::cmp::min;
use std::fmt;
@@ -9,11 +9,10 @@ use std::rc::Rc;
use std::sync::Arc;
use wasmparser::{OperatorValidatorConfig, ValidatingParserConfig};
use wasmtime_environ::settings::{self, Configurable};
use wasmtime_environ::CacheConfig;
use wasmtime_environ::Tunables;
use wasmtime_environ::{CacheConfig, Tunables};
use wasmtime_jit::{native, CompilationStrategy, Compiler};
use wasmtime_profiling::{JitDumpAgent, NullProfilerAgent, ProfilingAgent, VTuneAgent};
use wasmtime_runtime::{debug_builtins, RuntimeMemoryCreator};
use wasmtime_runtime::{debug_builtins, RuntimeMemoryCreator, VMInterrupts};
// Runtime Environment
@@ -33,6 +32,7 @@ pub struct Config {
pub(crate) cache_config: CacheConfig,
pub(crate) profiler: Arc<dyn ProfilingAgent>,
pub(crate) memory_creator: Option<MemoryCreatorProxy>,
pub(crate) max_wasm_stack: usize,
}
impl Config {
@@ -66,6 +66,11 @@ impl Config {
.set("opt_level", "speed")
.expect("should be valid flag");
// We don't use probestack as a stack limit mechanism
flags
.set("enable_probestack", "false")
.expect("should be valid flag");
Config {
tunables,
validating_config: ValidatingParserConfig {
@@ -82,6 +87,7 @@ impl Config {
cache_config: CacheConfig::new_cache_disabled(),
profiler: Arc::new(NullProfilerAgent),
memory_creator: None,
max_wasm_stack: 1 << 20,
}
}
@@ -94,6 +100,37 @@ impl Config {
self
}
/// Configures whether functions and loops will be interruptable via the
/// [`Store::interrupt_handle`] method.
///
/// For more information see the documentation on
/// [`Store::interrupt_handle`].
///
/// By default this option is `false`.
pub fn interruptable(&mut self, enable: bool) -> &mut Self {
self.tunables.interruptable = enable;
self
}
/// Configures the maximum amount of native stack space available to
/// executing WebAssembly code.
///
/// WebAssembly code currently executes on the native call stack for its own
/// call frames. WebAssembly, however, also has well-defined semantics on
/// stack overflow. This is intended to be a knob which can help configure
/// how much native stack space a wasm module is allowed to consume. Note
/// that the number here is not super-precise, but rather wasm will take at
/// most "pretty close to this much" stack space.
///
/// If a wasm call (or series of nested wasm calls) take more stack space
/// than the `size` specified then a stack overflow trap will be raised.
///
/// By default this option is 1 MB.
pub fn max_wasm_stack(&mut self, size: usize) -> &mut Self {
self.max_wasm_stack = size;
self
}
/// Configures whether the WebAssembly threads proposal will be enabled for
/// compilation.
///
@@ -552,6 +589,97 @@ impl Store {
pub fn same(a: &Store, b: &Store) -> bool {
Rc::ptr_eq(&a.inner, &b.inner)
}
/// Creates an [`InterruptHandle`] which can be used to interrupt the
/// execution of instances within this `Store`.
///
/// An [`InterruptHandle`] handle is a mechanism of ensuring that guest code
/// doesn't execute for too long. For example it's used to prevent wasm
/// programs for executing infinitely in infinite loops or recursive call
/// chains.
///
/// The [`InterruptHandle`] type is sendable to other threads so you can
/// interact with it even while the thread with this `Store` is executing
/// wasm code.
///
/// There's one method on an interrupt handle:
/// [`InterruptHandle::interrupt`]. This method is used to generate an
/// interrupt and cause wasm code to exit "soon".
///
/// ## When are interrupts delivered?
///
/// The term "interrupt" here refers to one of two different behaviors that
/// are interrupted in wasm:
///
/// * The head of every loop in wasm has a check to see if it's interrupted.
/// * The prologue of every function has a check to see if it's interrupted.
///
/// This interrupt mechanism makes no attempt to signal interrupts to
/// native code. For example if a host function is blocked, then sending
/// an interrupt will not interrupt that operation.
///
/// Interrupts are consumed as soon as possible when wasm itself starts
/// executing. This means that if you interrupt wasm code then it basically
/// guarantees that the next time wasm is executing on the target thread it
/// will return quickly (either normally if it were already in the process
/// of returning or with a trap from the interrupt). Once an interrupt
/// trap is generated then an interrupt is consumed, and further execution
/// will not be interrupted (unless another interrupt is set).
///
/// When implementing interrupts you'll want to ensure that the delivery of
/// interrupts into wasm code is also handled in your host imports and
/// functionality. Host functions need to either execute for bounded amounts
/// of time or you'll need to arrange for them to be interrupted as well.
///
/// ## Return Value
///
/// This function returns a `Result` since interrupts are not always
/// enabled. Interrupts are enabled via the [`Config::interruptable`]
/// method, and if this store's [`Config`] hasn't been configured to enable
/// interrupts then an error is returned.
///
/// ## Examples
///
/// ```
/// # use anyhow::Result;
/// # use wasmtime::*;
/// # fn main() -> Result<()> {
/// // Enable interruptable code via `Config` and then create an interrupt
/// // handle which we'll use later to interrupt running code.
/// let engine = Engine::new(Config::new().interruptable(true));
/// let store = Store::new(&engine);
/// let interrupt_handle = store.interrupt_handle()?;
///
/// // Compile and instantiate a small example with an infinite loop.
/// let module = Module::new(&store, r#"
/// (func (export "run") (loop br 0))
/// "#)?;
/// let instance = Instance::new(&module, &[])?;
/// let run = instance
/// .get_func("run")
/// .ok_or(anyhow::format_err!("failed to find `run` function export"))?
/// .get0::<()>()?;
///
/// // Spin up a thread to send us an interrupt in a second
/// std::thread::spawn(move || {
/// std::thread::sleep(std::time::Duration::from_secs(1));
/// interrupt_handle.interrupt();
/// });
///
/// let trap = run().unwrap_err();
/// assert!(trap.message().contains("wasm trap: interrupt"));
/// # Ok(())
/// # }
/// ```
pub fn interrupt_handle(&self) -> Result<InterruptHandle> {
if self.engine().config.tunables.interruptable {
Ok(InterruptHandle {
interrupts: self.compiler().interrupts().clone(),
})
} else {
bail!("interrupts aren't enabled for this `Store`")
}
}
}
impl Default for Store {
@@ -560,10 +688,32 @@ impl Default for Store {
}
}
/// A threadsafe handle used to interrupt instances executing within a
/// particular `Store`.
///
/// This structure is created by the [`Store::interrupt_handle`] method.
pub struct InterruptHandle {
interrupts: Arc<VMInterrupts>,
}
impl InterruptHandle {
/// Flags that execution within this handle's original [`Store`] should be
/// interrupted.
///
/// This will not immediately interrupt execution of wasm modules, but
/// rather it will interrupt wasm execution of loop headers and wasm
/// execution of function entries. For more information see
/// [`Store::interrupt_handle`].
pub fn interrupt(&self) {
self.interrupts.interrupt()
}
}
fn _assert_send_sync() {
fn _assert<T: Send + Sync>() {}
_assert::<Engine>();
_assert::<Config>();
_assert::<InterruptHandle>();
}
#[cfg(test)]

View File

@@ -53,6 +53,8 @@ pub(crate) fn create_handle(
.operator_config
.enable_bulk_memory,
state,
store.compiler().interrupts().clone(),
store.engine().config().max_wasm_stack,
)?)
}
}

View File

@@ -50,11 +50,18 @@ impl Trap {
.downcast()
.expect("only `Trap` user errors are supported")
}
wasmtime_runtime::Trap::Jit { pc, backtrace } => {
let code = info
wasmtime_runtime::Trap::Jit {
pc,
backtrace,
maybe_interrupted,
} => {
let mut code = info
.lookup_trap_info(pc)
.map(|info| info.trap_code)
.unwrap_or(TrapCode::StackOverflow);
if maybe_interrupted && code == TrapCode::StackOverflow {
code = TrapCode::Interrupt;
}
Trap::new_wasm(&info, Some(pc), code, backtrace)
}
wasmtime_runtime::Trap::Wasm {

135
crates/api/tests/iloop.rs Normal file
View File

@@ -0,0 +1,135 @@
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use wasmtime::*;
fn interruptable_store() -> Store {
let engine = Engine::new(Config::new().interruptable(true));
Store::new(&engine)
}
fn hugely_recursive_module(store: &Store) -> anyhow::Result<Module> {
let mut wat = String::new();
wat.push_str(
r#"
(import "" "" (func))
(func (export "loop") call 2 call 2)
"#,
);
for i in 0..100 {
wat.push_str(&format!("(func call {0} call {0})\n", i + 3));
}
wat.push_str("(func call 0)\n");
Module::new(&store, &wat)
}
#[test]
fn loops_interruptable() -> anyhow::Result<()> {
let store = interruptable_store();
let module = Module::new(&store, r#"(func (export "loop") (loop br 0))"#)?;
let instance = Instance::new(&module, &[])?;
let iloop = instance.get_func("loop").unwrap().get0::<()>()?;
store.interrupt_handle()?.interrupt();
let trap = iloop().unwrap_err();
assert!(trap.message().contains("wasm trap: interrupt"));
Ok(())
}
#[test]
fn functions_interruptable() -> anyhow::Result<()> {
let store = interruptable_store();
let module = hugely_recursive_module(&store)?;
let func = Func::wrap(&store, || {});
let instance = Instance::new(&module, &[func.into()])?;
let iloop = instance.get_func("loop").unwrap().get0::<()>()?;
store.interrupt_handle()?.interrupt();
let trap = iloop().unwrap_err();
assert!(
trap.message().contains("wasm trap: interrupt"),
"{}",
trap.message()
);
Ok(())
}
#[test]
fn loop_interrupt_from_afar() -> anyhow::Result<()> {
// Create an instance which calls an imported function on each iteration of
// the loop so we can count the number of loop iterations we've executed so
// far.
static HITS: AtomicUsize = AtomicUsize::new(0);
let store = interruptable_store();
let module = Module::new(
&store,
r#"
(import "" "" (func))
(func (export "loop")
(loop
call 0
br 0)
)
"#,
)?;
let func = Func::wrap(&store, || {
HITS.fetch_add(1, SeqCst);
});
let instance = Instance::new(&module, &[func.into()])?;
// Use the instance's interrupt handle to wait for it to enter the loop long
// enough and then we signal an interrupt happens.
let handle = store.interrupt_handle()?;
let thread = std::thread::spawn(move || {
while HITS.load(SeqCst) <= 100_000 {
// continue ...
}
handle.interrupt();
});
// Enter the infinitely looping function and assert that our interrupt
// handle does indeed actually interrupt the function.
let iloop = instance.get_func("loop").unwrap().get0::<()>()?;
let trap = iloop().unwrap_err();
thread.join().unwrap();
assert!(
trap.message().contains("wasm trap: interrupt"),
"bad message: {}",
trap.message()
);
Ok(())
}
#[test]
fn function_interrupt_from_afar() -> anyhow::Result<()> {
// Create an instance which calls an imported function on each iteration of
// the loop so we can count the number of loop iterations we've executed so
// far.
static HITS: AtomicUsize = AtomicUsize::new(0);
let store = interruptable_store();
let module = hugely_recursive_module(&store)?;
let func = Func::wrap(&store, || {
HITS.fetch_add(1, SeqCst);
});
let instance = Instance::new(&module, &[func.into()])?;
// Use the instance's interrupt handle to wait for it to enter the loop long
// enough and then we signal an interrupt happens.
let handle = store.interrupt_handle()?;
let thread = std::thread::spawn(move || {
while HITS.load(SeqCst) <= 100_000 {
// continue ...
}
handle.interrupt();
});
// Enter the infinitely looping function and assert that our interrupt
// handle does indeed actually interrupt the function.
let iloop = instance.get_func("loop").unwrap().get0::<()>()?;
let trap = iloop().unwrap_err();
thread.join().unwrap();
assert!(
trap.message().contains("wasm trap: interrupt"),
"bad message: {}",
trap.message()
);
Ok(())
}

View File

@@ -0,0 +1,60 @@
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use wasmtime::*;
#[test]
fn host_always_has_some_stack() -> anyhow::Result<()> {
static HITS: AtomicUsize = AtomicUsize::new(0);
// assume hosts always have at least 512k of stack
const HOST_STACK: usize = 512 * 1024;
let store = Store::default();
// Create a module that's infinitely recursive, but calls the host on each
// level of wasm stack to always test how much host stack we have left.
let module = Module::new(
&store,
r#"
(module
(import "" "" (func $host))
(func $recursive (export "foo")
call $host
call $recursive)
)
"#,
)?;
let func = Func::wrap(&store, test_host_stack);
let instance = Instance::new(&module, &[func.into()])?;
let foo = instance.get_func("foo").unwrap().get0::<()>()?;
// Make sure that our function traps and the trap says that the call stack
// has been exhausted.
let trap = foo().unwrap_err();
assert!(
trap.message().contains("call stack exhausted"),
"{}",
trap.message()
);
// Additionally, however, and this is the crucial test, make sure that the
// host function actually completed. If HITS is 1 then we entered but didn't
// exit meaning we segfaulted while executing the host, yet still tried to
// recover from it with longjmp.
assert_eq!(HITS.load(SeqCst), 0);
return Ok(());
fn test_host_stack() {
HITS.fetch_add(1, SeqCst);
assert!(consume_some_stack(0, HOST_STACK) > 0);
HITS.fetch_sub(1, SeqCst);
}
#[inline(never)]
fn consume_some_stack(ptr: usize, stack: usize) -> usize {
if stack == 0 {
return ptr;
}
let mut space = [0u8; 1024];
consume_some_stack(space.as_mut_ptr() as usize, stack.saturating_sub(1024))
}
}