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

@@ -1,5 +1,90 @@
//! Support for compiling with Cranelift.
// # How does Wasmtime prevent stack overflow?
//
// A few locations throughout the codebase link to this file to explain
// interrupts and stack overflow. To start off, let's take a look at stack
// overflow. Wasm code is well-defined to have stack overflow being recoverable
// and raising a trap, so we need to handle this somehow! There's also an added
// constraint where as an embedder you frequently are running host-provided
// code called from wasm. WebAssembly and native code currently share the same
// call stack, so you want to make sure that your host-provided code will have
// enough call-stack available to it.
//
// Given all that, the way that stack overflow is handled is by adding a
// prologue check to all JIT functions for how much native stack is remaining.
// The `VMContext` pointer is the first argument to all functions, and the first
// field of this structure is `*const VMInterrupts` and the first field of that
// is the stack limit. Note that the stack limit in this case means "if the
// stack pointer goes below this, trap". Each JIT function which consumes stack
// space or isn't a leaf function starts off by loading the stack limit,
// checking it against the stack pointer, and optionally traps.
//
// This manual check allows the embedder (us) to give wasm a relatively precise
// amount of stack allocation. Using this scheme we reserve a chunk of stack
// for wasm code relative from where wasm code was called. This ensures that
// native code called by wasm should have native stack space to run, and the
// numbers of stack spaces here should all be configurable for various
// embeddings.
//
// Note that we do not consider each thread's stack guard page here. It's
// considered that if you hit that you still abort the whole program. This
// shouldn't happen most of the time because wasm is always stack-bound and
// it's up to the embedder to bound its own native stack.
//
// So all-in-all, that's how we implement stack checks. Note that stack checks
// cannot be disabled because it's a feature of core wasm semantics. This means
// that all functions almost always have a stack check prologue, and it's up to
// us to optimize away that cost as much as we can.
//
// For more information about the tricky bits of managing the reserved stack
// size of wasm, see the implementation in `traphandlers.rs` in the
// `update_stack_limit` function.
//
// # How is Wasmtime interrupted?
//
// Ok so given all that background of stack checks, the next thing we want to
// build on top of this is the ability to *interrupt* executing wasm code. This
// is useful to ensure that wasm always executes within a particular time slice
// or otherwise doesn't consume all CPU resources on a system. There are two
// major ways that interrupts are required:
//
// * Loops - likely immediately apparent but it's easy to write an infinite
// loop in wasm, so we need the ability to interrupt loops.
// * Function entries - somewhat more subtle, but imagine a module where each
// function calls the next function twice. This creates 2^n calls pretty
// quickly, so a pretty small module can export a function with no loops
// that takes an extremely long time to call.
//
// In many cases if an interrupt comes in you want to interrupt host code as
// well, but we're explicitly not considering that here. We're hoping that
// interrupting host code is largely left to the embedder (e.g. figuring out
// how to interrupt blocking syscalls) and they can figure that out. The purpose
// of this feature is to basically only give the ability to interrupt
// currently-executing wasm code (or triggering an interrupt as soon as wasm
// reenters itself).
//
// To implement interruption of loops we insert code at the head of all loops
// which checks the stack limit counter. If the counter matches a magical
// sentinel value that's impossible to be the real stack limit, then we
// interrupt the loop and trap. To implement interrupts of functions, we
// actually do the same thing where the magical sentinel value we use here is
// automatically considered as considering all stack pointer values as "you ran
// over your stack". This means that with a write of a magical value to one
// location we can interrupt both loops and function bodies.
//
// The "magical value" here is `usize::max_value() - N`. We reserve
// `usize::max_value()` for "the stack limit isn't set yet" and so -N is
// then used for "you got interrupted". We do a bit of patching afterwards to
// translate a stack overflow into an interrupt trap if we see that an
// interrupt happened. Note that `N` here is a medium-size-ish nonzero value
// chosen in coordination with the cranelift backend. Currently it's 32k. The
// value of N is basically a threshold in the backend for "anything less than
// this requires only one branch in the prologue, any stack size bigger requires
// two branches". Naturally we want most functions to have one branch, but we
// also need to actually catch stack overflow, so for now 32k is chosen and it's
// assume no valid stack pointer will ever be `usize::max_value() - 32k`.
use crate::address_map::{FunctionAddressMap, InstructionAddressMap};
use crate::cache::{ModuleCacheDataTupleType, ModuleCacheEntry};
use crate::compilation::{
@@ -13,6 +98,7 @@ use cranelift_codegen::{binemit, isa, Context};
use cranelift_entity::PrimaryMap;
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, FuncTranslator, ModuleTranslationState};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
/// Implementation of a relocation sink that just saves all the information for later
@@ -208,12 +294,47 @@ fn compile(env: CompileEnv<'_>) -> Result<ModuleCacheDataTupleType, CompileError
context.func.collect_debug_info();
}
let mut func_env = FuncEnvironment::new(isa.frontend_config(), env.local, env.tunables);
// We use these as constant offsets below in
// `stack_limit_from_arguments`, so assert their values here. This
// allows the closure below to get coerced to a function pointer, as
// needed by `ir::Function`.
//
// Otherwise our stack limit is specially calculated from the vmctx
// argument, where we need to load the `*const VMInterrupts`
// pointer, and then from that pointer we need to load the stack
// limit itself. Note that manual register allocation is needed here
// too due to how late in the process this codegen happens.
//
// For more information about interrupts and stack checks, see the
// top of this file.
let vmctx = context
.func
.create_global_value(ir::GlobalValueData::VMContext);
let interrupts_ptr = context.func.create_global_value(ir::GlobalValueData::Load {
base: vmctx,
offset: i32::try_from(func_env.offsets.vmctx_interrupts())
.unwrap()
.into(),
global_type: isa.pointer_type(),
readonly: true,
});
let stack_limit = context.func.create_global_value(ir::GlobalValueData::Load {
base: interrupts_ptr,
offset: i32::try_from(func_env.offsets.vminterrupts_stack_limit())
.unwrap()
.into(),
global_type: isa.pointer_type(),
readonly: false,
});
context.func.stack_limit = Some(stack_limit);
func_translator.translate(
env.module_translation.0,
input.data,
input.module_offset,
&mut context.func,
&mut FuncEnvironment::new(isa.frontend_config(), env.local),
&mut func_env,
)?;
let mut code_buf: Vec<u8> = Vec::new();

View File

@@ -1,6 +1,6 @@
use crate::module::{MemoryPlan, MemoryStyle, ModuleLocal, TableStyle};
use crate::vmoffsets::VMOffsets;
use crate::WASM_PAGE_SIZE;
use crate::{Tunables, INTERRUPTED, WASM_PAGE_SIZE};
use cranelift_codegen::cursor::FuncCursor;
use cranelift_codegen::ir;
use cranelift_codegen::ir::condcodes::*;
@@ -135,13 +135,16 @@ pub struct FuncEnvironment<'module_environment> {
data_drop_sig: Option<ir::SigRef>,
/// Offsets to struct fields accessed by JIT code.
offsets: VMOffsets,
pub(crate) offsets: VMOffsets,
tunables: &'module_environment Tunables,
}
impl<'module_environment> FuncEnvironment<'module_environment> {
pub fn new(
target_config: TargetFrontendConfig,
module: &'module_environment ModuleLocal,
tunables: &'module_environment Tunables,
) -> Self {
Self {
target_config,
@@ -157,6 +160,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
memory_init_sig: None,
data_drop_sig: None,
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
tunables,
}
}
@@ -1246,4 +1250,37 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
Ok(())
}
fn translate_loop_header(&mut self, mut pos: FuncCursor) -> WasmResult<()> {
if !self.tunables.interruptable {
return Ok(());
}
// Start out each loop with a check to the interupt flag to allow
// interruption of long or infinite loops.
//
// For more information about this see comments in
// `crates/environ/src/cranelift.rs`
let vmctx = self.vmctx(&mut pos.func);
let pointer_type = self.pointer_type();
let base = pos.ins().global_value(pointer_type, vmctx);
let offset = i32::try_from(self.offsets.vmctx_interrupts()).unwrap();
let interrupt_ptr = pos
.ins()
.load(pointer_type, ir::MemFlags::trusted(), base, offset);
let interrupt = pos.ins().load(
pointer_type,
ir::MemFlags::trusted(),
interrupt_ptr,
i32::from(self.offsets.vminterrupts_stack_limit()),
);
// Note that the cast to `isize` happens first to allow sign-extension,
// if necessary, to `i64`.
let interrupted_sentinel = pos.ins().iconst(pointer_type, INTERRUPTED as isize as i64);
let cmp = pos
.ins()
.icmp(IntCC::Equal, interrupt, interrupted_sentinel);
pos.ins().trapnz(cmp, ir::TrapCode::Interrupt);
Ok(())
}
}

View File

@@ -62,7 +62,7 @@ pub use crate::module_environ::{
ModuleEnvironment, ModuleTranslation,
};
pub use crate::tunables::Tunables;
pub use crate::vmoffsets::{TargetSharedSignatureIndex, VMOffsets};
pub use crate::vmoffsets::{TargetSharedSignatureIndex, VMOffsets, INTERRUPTED};
/// WebAssembly page sizes are defined to be 64KiB.
pub const WASM_PAGE_SIZE: u32 = 0x10000;

View File

@@ -26,7 +26,11 @@ impl crate::compilation::Compiler for Lightbeam {
return Err(CompileError::DebugInfoNotSupported);
}
let env = FuncEnvironment::new(isa.frontend_config(), &translation.module.local);
let env = FuncEnvironment::new(
isa.frontend_config(),
&translation.module.local,
&translation.tunables,
);
let mut relocations = PrimaryMap::new();
let mut codegen_session: lightbeam::CodeGenSession<_> =
lightbeam::CodeGenSession::new(translation.function_body_inputs.len() as u32, &env);

View File

@@ -1,4 +1,3 @@
use crate::func_environ::FuncEnvironment;
use crate::module::{EntityIndex, MemoryPlan, Module, TableElements, TablePlan};
use crate::tunables::Tunables;
use cranelift_codegen::ir;
@@ -46,13 +45,6 @@ pub struct ModuleTranslation<'data> {
pub module_translation: Option<ModuleTranslationState>,
}
impl<'data> ModuleTranslation<'data> {
/// Return a new `FuncEnvironment` for translating a function.
pub fn func_env(&self) -> FuncEnvironment<'_> {
FuncEnvironment::new(self.target_config, &self.module.local)
}
}
/// Object containing the standalone environment information.
pub struct ModuleEnvironment<'data> {
/// The result to be filled in.

View File

@@ -12,6 +12,14 @@ pub struct Tunables {
/// Whether or not to generate DWARF debug information.
pub debug_info: bool,
/// Whether or not to enable the ability to interrupt wasm code dynamically.
///
/// More info can be found about the implementation in
/// crates/environ/src/cranelift.rs. Note that you can't interrupt host
/// calls and interrupts are implemented through the `VMInterrupts`
/// structure, or `InterruptHandle` in the `wasmtime` crate.
pub interruptable: bool,
}
impl Default for Tunables {
@@ -44,6 +52,7 @@ impl Default for Tunables {
dynamic_memory_offset_guard_size: 0x1_0000,
debug_info: false,
interruptable: false,
}
}
}

View File

@@ -1,6 +1,21 @@
//! Offsets and sizes of various structs in wasmtime-runtime's vmcontext
//! module.
// Currently the `VMContext` allocation by field looks like this:
//
// struct VMContext {
// interrupts: *const VMInterrupts,
// signature_ids: [VMSharedSignatureIndex; module.num_signature_ids],
// imported_functions: [VMFunctionImport; module.num_imported_functions],
// imported_tables: [VMTableImport; module.num_imported_tables],
// imported_memories: [VMMemoryImport; module.num_imported_memories],
// imported_globals: [VMGlobalImport; module.num_imported_globals],
// tables: [VMTableDefinition; module.num_defined_tables],
// memories: [VMMemoryDefinition; module.num_defined_memories],
// globals: [VMGlobalDefinition; module.num_defined_globals],
// builtins: VMBuiltinFunctionsArray,
// }
use crate::module::ModuleLocal;
use crate::BuiltinFunctionIndex;
use cranelift_codegen::ir;
@@ -11,6 +26,11 @@ use cranelift_wasm::{
use more_asserts::assert_lt;
use std::convert::TryFrom;
/// Sentinel value indicating that wasm has been interrupted.
// Note that this has a bit of an odd definition. See the `insert_stack_check`
// function in `cranelift/codegen/src/isa/x86/abi.rs` for more information
pub const INTERRUPTED: usize = usize::max_value() - 32 * 1024;
#[cfg(target_pointer_width = "32")]
fn cast_to_u32(sz: usize) -> u32 {
u32::try_from(sz).unwrap()
@@ -226,6 +246,14 @@ impl VMOffsets {
}
}
/// Offsets for `VMInterrupts`.
impl VMOffsets {
/// Return the offset of the `stack_limit` field of `VMInterrupts`
pub fn vminterrupts_stack_limit(&self) -> u8 {
0
}
}
/// Offsets for `VMCallerCheckedAnyfunc`.
impl VMOffsets {
/// The offset of the `func_ptr` field.
@@ -253,9 +281,16 @@ impl VMOffsets {
/// Offsets for `VMContext`.
impl VMOffsets {
/// Return the offset to the `VMInterrupts` structure
pub fn vmctx_interrupts(&self) -> u32 {
0
}
/// The offset of the `signature_ids` array.
pub fn vmctx_signature_ids_begin(&self) -> u32 {
0
self.vmctx_interrupts()
.checked_add(u32::from(self.pointer_size))
.unwrap()
}
/// The offset of the `tables` array.