From 5d39eb45f33eda29d61a4b98eb63b751760a154a Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Apr 2020 09:20:16 -0700 Subject: [PATCH 01/13] 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 --- cranelift/codegen/src/ir/function.rs | 15 + cranelift/codegen/src/isa/x86/abi.rs | 72 +++-- .../filetests/isa/x86/prologue-epilogue.clif | 28 +- cranelift/reader/src/run_command.rs | 5 +- crates/api/src/func.rs | 23 +- crates/api/src/instance.rs | 1 + crates/api/src/runtime.rs | 159 ++++++++++- crates/api/src/trampoline/create_handle.rs | 2 + crates/api/src/trap.rs | 11 +- crates/api/tests/iloop.rs | 155 ++++++++++ crates/api/tests/stack-overflow.rs | 65 +++++ crates/c-api/include/wasmtime.h | 19 ++ crates/c-api/src/config.rs | 10 + crates/c-api/src/store.rs | 23 +- crates/environ/src/cranelift.rs | 116 +++++++- crates/environ/src/func_environ.rs | 39 ++- crates/environ/src/lib.rs | 2 +- crates/environ/src/lightbeam.rs | 6 +- crates/environ/src/module_environ.rs | 8 - crates/environ/src/tunables.rs | 4 + crates/environ/src/vmoffsets.rs | 37 ++- crates/fuzzing/src/generators.rs | 4 +- crates/fuzzing/src/generators/api.rs | 13 +- crates/fuzzing/src/oracles.rs | 5 + crates/jit/src/compiler.rs | 11 +- crates/jit/src/instantiate.rs | 8 + crates/jit/src/link.rs | 36 --- crates/runtime/src/instance.rs | 34 ++- crates/runtime/src/lib.rs | 4 +- crates/runtime/src/traphandlers.rs | 266 +++++++----------- crates/runtime/src/vmcontext.rs | 47 ++++ crates/wiggle/src/lib.rs | 6 +- examples/interrupt.c | 141 ++++++++++ examples/interrupt.rs | 39 +++ examples/interrupt.wat | 6 + 35 files changed, 1158 insertions(+), 262 deletions(-) create mode 100644 crates/api/tests/iloop.rs create mode 100644 crates/api/tests/stack-overflow.rs create mode 100644 examples/interrupt.c create mode 100644 examples/interrupt.rs create mode 100644 examples/interrupt.wat diff --git a/cranelift/codegen/src/ir/function.rs b/cranelift/codegen/src/ir/function.rs index db989818f34a..e27a9be2c823 100644 --- a/cranelift/codegen/src/ir/function.rs +++ b/cranelift/codegen/src/ir/function.rs @@ -4,6 +4,7 @@ //! instructions. use crate::binemit::CodeOffset; +use crate::cursor::EncCursor; use crate::entity::{PrimaryMap, SecondaryMap}; use crate::ir; use crate::ir::{ @@ -95,6 +96,18 @@ pub struct Function { /// /// This is used for some ABIs to generate unwind information. pub epilogues_start: Vec, + + /// An optional closure which calculates the stack limit for this function + /// from the arguments of the function. + /// + /// When configured a stack check will be emitted in the prologue of this + /// function, trapping if the stack check fails. This closure, if specified, + /// can be then used to infer the stack limit from provided arguments as an + /// alternative to using `ArgumentPurpose::StackLimit`. + /// + /// The first argument is the cursor we're encoding the prologue too, and + /// the second argument is a scratch register location if necessary. + pub stack_limit_from_arguments: Option ir::Value>, } impl Function { @@ -119,6 +132,7 @@ impl Function { srclocs: SecondaryMap::new(), prologue_end: None, epilogues_start: Vec::new(), + stack_limit_from_arguments: None, } } @@ -140,6 +154,7 @@ impl Function { self.srclocs.clear(); self.prologue_end = None; self.epilogues_start.clear(); + self.stack_limit_from_arguments = None; } /// Create a new empty, anonymous function with a Fast calling convention. diff --git a/cranelift/codegen/src/isa/x86/abi.rs b/cranelift/codegen/src/isa/x86/abi.rs index cfa698698d39..224305e3be0b 100644 --- a/cranelift/codegen/src/isa/x86/abi.rs +++ b/cranelift/codegen/src/isa/x86/abi.rs @@ -685,21 +685,32 @@ fn insert_common_prologue( fpr_slot: Option<&StackSlot>, isa: &dyn TargetIsa, ) { - if stack_size > 0 { - // Check if there is a special stack limit parameter. If so insert stack check. - if let Some(stack_limit_arg) = pos.func.special_param(ArgumentPurpose::StackLimit) { - // Total stack size is the size of all stack area used by the function, including - // pushed CSRs, frame pointer. - // Also, the size of a return address, implicitly pushed by a x86 `call` instruction, - // also should be accounted for. - // If any FPR are present, count them as well as necessary alignment space. - // TODO: Check if the function body actually contains a `call` instruction. - let mut total_stack_size = - (csrs.iter(GPR).len() + 1 + 1) as i64 * (isa.pointer_bytes() as isize) as i64; - - total_stack_size += csrs.iter(FPR).len() as i64 * types::F64X2.bytes() as i64; - - insert_stack_check(pos, total_stack_size, stack_limit_arg); + // If this is a leaf function with zero stack, then there's no need to + // insert a stack check since it can't overflow anything and + // forward-progress is guarantee so long as loop are handled anyway. + // + // If this has a stack size it could stack overflow, or if it isn't a leaf + // it could be part of a long call chain which we need to check anyway. + // + // First we look for the stack limit as a special argument to the function, + // and failing that we see if a custom stack limit factory has been provided + // which will be used to likely calculate the stack limit from the arguments + // or perhaps constants. + if stack_size > 0 || !pos.func.is_leaf() { + let scratch = ir::ValueLoc::Reg(RU::rax as RegUnit); + let stack_limit_arg = match pos.func.special_param(ArgumentPurpose::StackLimit) { + Some(arg) => { + let copy = pos.ins().copy(arg); + pos.func.locations[copy] = scratch; + Some(copy) + } + None => pos + .func + .stack_limit_from_arguments + .map(|closure| closure(pos, scratch)), + }; + if let Some(stack_limit_arg) = stack_limit_arg { + insert_stack_check(pos, stack_size, stack_limit_arg); } } @@ -816,11 +827,36 @@ fn insert_common_prologue( fn insert_stack_check(pos: &mut EncCursor, stack_size: i64, stack_limit_arg: ir::Value) { use crate::ir::condcodes::IntCC; + // Our stack pointer, after subtracting `stack_size`, must not be below + // `stack_limit_arg`. To do this we're going to add `stack_size` to + // `stack_limit_arg` and see if the stack pointer is below that. The + // `stack_size + stack_limit_arg` computation might overflow, however, due + // to how stack limits may be loaded and set externally to trigger a trap. + // + // To handle this we'll need an extra comparison to see if the stack + // pointer is already below `stack_limit_arg`. Most of the time this + // isn't necessary though since the stack limit which triggers a trap is + // likely a sentinel somewhere around `usize::max_value()`. In that case + // only conditionally emit this pre-flight check. That way most functions + // only have the one comparison, but are also guaranteed that if we add + // `stack_size` to `stack_limit_arg` is won't overflow. + // + // This does mean that code generators which use this stack check + // functionality need to ensure that values stored into the stack limit + // will never overflow if this threshold is added. + if stack_size >= 32 * 1024 { + let cflags = pos.ins().ifcmp_sp(stack_limit_arg); + pos.func.locations[cflags] = ir::ValueLoc::Reg(RU::rflags as RegUnit); + pos.ins().trapif( + IntCC::UnsignedGreaterThanOrEqual, + cflags, + ir::TrapCode::StackOverflow, + ); + } + // Copy `stack_limit_arg` into a %rax and use it for calculating // a SP threshold. - let stack_limit_copy = pos.ins().copy(stack_limit_arg); - pos.func.locations[stack_limit_copy] = ir::ValueLoc::Reg(RU::rax as RegUnit); - let sp_threshold = pos.ins().iadd_imm(stack_limit_copy, stack_size); + let sp_threshold = pos.ins().iadd_imm(stack_limit_arg, stack_size); pos.func.locations[sp_threshold] = ir::ValueLoc::Reg(RU::rax as RegUnit); // If the stack pointer currently reaches the SP threshold or below it then after opening diff --git a/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif b/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif index 25118ca72b26..3804ad3e77f4 100644 --- a/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif +++ b/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif @@ -1,6 +1,7 @@ test compile set opt_level=speed_and_size set is_pic +set enable_probestack=false target x86_64 haswell ; An empty function. @@ -244,7 +245,7 @@ block0(v0: i64): ; nextln: ; nextln: block0(v0: i64 [%rdi], v4: i64 [%rbp]): ; nextln: v1 = copy v0 -; nextln: v2 = iadd_imm v1, 16 +; nextln: v2 = iadd_imm v1, 176 ; nextln: v3 = ifcmp_sp v2 ; nextln: trapif uge v3, stk_ovf ; nextln: x86_push v4 @@ -254,3 +255,28 @@ block0(v0: i64): ; nextln: v5 = x86_pop.i64 ; nextln: return v5 ; nextln: } + +function %big_stack_limit(i64 stack_limit) { + ss0 = explicit_slot 40000 +block0(v0: i64): + return +} + +; check: function %big_stack_limit(i64 stack_limit [%rdi], i64 fp [%rbp]) -> i64 fp [%rbp] fast { +; nextln: ss0 = explicit_slot 40000, offset -40016 +; nextln: ss1 = incoming_arg 16, offset -16 +; nextln: +; nextln: block0(v0: i64 [%rdi], v5: i64 [%rbp]): +; nextln: v1 = copy v0 +; nextln: v2 = ifcmp_sp v1 +; nextln: trapif uge v2, stk_ovf +; nextln: v3 = iadd_imm v1, 0x9c40 +; nextln: v4 = ifcmp_sp v3 +; nextln: trapif uge v4, stk_ovf +; nextln: x86_push v5 +; nextln: copy_special %rsp -> %rbp +; nextln: adjust_sp_down_imm 0x9c40 +; nextln: adjust_sp_up_imm 0x9c40 +; nextln: v6 = x86_pop.i64 +; nextln: return v6 +; nextln: } diff --git a/cranelift/reader/src/run_command.rs b/cranelift/reader/src/run_command.rs index 4f106d956482..0e2c9c45c3ee 100644 --- a/cranelift/reader/src/run_command.rs +++ b/cranelift/reader/src/run_command.rs @@ -12,7 +12,8 @@ use std::fmt::{Display, Formatter, Result}; /// A run command appearing in a test file. /// -/// For parsing, see [Parser::parse_run_command]. +/// For parsing, see +/// [Parser::parse_run_command](crate::parser::Parser::parse_run_command). #[derive(PartialEq, Debug)] pub enum RunCommand { /// Invoke a function and print its result. @@ -66,6 +67,8 @@ impl Display for Invocation { /// Represent a data value. Where [Value] is an SSA reference, [DataValue] is the type + value /// that would be referred to by a [Value]. +/// +/// [Value]: cranelift_codegen::ir::Value #[allow(missing_docs)] #[derive(Clone, Debug, PartialEq)] pub enum DataValue { diff --git a/crates/api/src/func.rs b/crates/api/src/func.rs index b6a4dec62f57..bb723480031f 100644 --- a/crates/api/src/func.rs +++ b/crates/api/src/func.rs @@ -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()); } diff --git a/crates/api/src/instance.rs b/crates/api/src/instance.rs index c1b66ebe211b..ec883d923eb7 100644 --- a/crates/api/src/instance.rs +++ b/crates/api/src/instance.rs @@ -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 { diff --git a/crates/api/src/runtime.rs b/crates/api/src/runtime.rs index 33f1a46cf508..be8fba056e3e 100644 --- a/crates/api/src/runtime.rs +++ b/crates/api/src/runtime.rs @@ -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, pub(crate) memory_creator: Option, + 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,98 @@ 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_export("run") + /// .and_then(|e| e.func()) + /// .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 { + 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 +689,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, +} + +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() {} _assert::(); _assert::(); + _assert::(); } #[cfg(test)] diff --git a/crates/api/src/trampoline/create_handle.rs b/crates/api/src/trampoline/create_handle.rs index d410a4c8b490..3fa2c68410c2 100644 --- a/crates/api/src/trampoline/create_handle.rs +++ b/crates/api/src/trampoline/create_handle.rs @@ -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, )?) } } diff --git a/crates/api/src/trap.rs b/crates/api/src/trap.rs index 322e38739d22..b1bdd6afde6c 100644 --- a/crates/api/src/trap.rs +++ b/crates/api/src/trap.rs @@ -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 { diff --git a/crates/api/tests/iloop.rs b/crates/api/tests/iloop.rs new file mode 100644 index 000000000000..d496b3c79d3d --- /dev/null +++ b/crates/api/tests/iloop.rs @@ -0,0 +1,155 @@ +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 { + 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_export("loop") + .unwrap() + .func() + .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_export("loop") + .unwrap() + .func() + .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_export("loop") + .unwrap() + .func() + .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_export("loop") + .unwrap() + .func() + .unwrap() + .get0::<()>()?; + let trap = iloop().unwrap_err(); + thread.join().unwrap(); + assert!( + trap.message().contains("wasm trap: interrupt"), + "bad message: {}", + trap.message() + ); + Ok(()) +} diff --git a/crates/api/tests/stack-overflow.rs b/crates/api/tests/stack-overflow.rs new file mode 100644 index 000000000000..9ec24b648f95 --- /dev/null +++ b/crates/api/tests/stack-overflow.rs @@ -0,0 +1,65 @@ +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_export("foo") + .unwrap() + .func() + .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)) + } +} diff --git a/crates/c-api/include/wasmtime.h b/crates/c-api/include/wasmtime.h index f8cf4ed6f223..dc8d91bf7227 100644 --- a/crates/c-api/include/wasmtime.h +++ b/crates/c-api/include/wasmtime.h @@ -49,6 +49,8 @@ enum wasmtime_profiling_strategy_t { // ProfilingStrategy WASM_API_EXTERN ret wasmtime_config_##name##_set(wasm_config_t*, ty); WASMTIME_CONFIG_PROP(void, debug_info, bool) +WASMTIME_CONFIG_PROP(void, interruptable, bool) +WASMTIME_CONFIG_PROP(void, max_wasm_stack, size_t) WASMTIME_CONFIG_PROP(void, wasm_threads, bool) WASMTIME_CONFIG_PROP(void, wasm_reference_types, bool) WASMTIME_CONFIG_PROP(void, wasm_simd, bool) @@ -131,6 +133,23 @@ WASM_API_EXTERN own wasm_func_t* wasmtime_func_new_with_env( WASM_API_EXTERN own wasm_extern_t* wasmtime_caller_export_get(const wasmtime_caller_t* caller, const wasm_name_t* name); +/////////////////////////////////////////////////////////////////////////////// +// +// wasmtime_interrupt_handle_t extension, allowing interruption of running wasm +// modules. +// +// Note that `wasmtime_interrupt_handle_t` is safe to send to other threads and +// interrupt/delete. +// +// Also note that `wasmtime_interrupt_handle_new` may return NULL if interrupts +// are not enabled in `wasm_config_t`. + +WASMTIME_DECLARE_OWN(interrupt_handle) + +WASM_API_EXTERN own wasmtime_interrupt_handle_t *wasmtime_interrupt_handle_new(wasm_store_t *store); + +WASM_API_EXTERN void wasmtime_interrupt_handle_interrupt(wasmtime_interrupt_handle_t *handle); + /////////////////////////////////////////////////////////////////////////////// // // Extensions to `wasm_frame_t` diff --git a/crates/c-api/src/config.rs b/crates/c-api/src/config.rs index 5a93fdcfb4e9..5ec6f9b6218f 100644 --- a/crates/c-api/src/config.rs +++ b/crates/c-api/src/config.rs @@ -44,6 +44,16 @@ pub extern "C" fn wasmtime_config_debug_info_set(c: &mut wasm_config_t, enable: c.config.debug_info(enable); } +#[no_mangle] +pub extern "C" fn wasmtime_config_interruptable_set(c: &mut wasm_config_t, enable: bool) { + c.config.interruptable(enable); +} + +#[no_mangle] +pub extern "C" fn wasmtime_config_max_wasm_stack_set(c: &mut wasm_config_t, size: usize) { + c.config.max_wasm_stack(size); +} + #[no_mangle] pub extern "C" fn wasmtime_config_wasm_threads_set(c: &mut wasm_config_t, enable: bool) { c.config.wasm_threads(enable); diff --git a/crates/c-api/src/store.rs b/crates/c-api/src/store.rs index 159df7da976d..a2a5b1c25b60 100644 --- a/crates/c-api/src/store.rs +++ b/crates/c-api/src/store.rs @@ -1,5 +1,5 @@ use crate::wasm_engine_t; -use wasmtime::{HostRef, Store}; +use wasmtime::{HostRef, InterruptHandle, Store}; #[repr(C)] #[derive(Clone)] @@ -16,3 +16,24 @@ pub extern "C" fn wasm_store_new(engine: &wasm_engine_t) -> Box { store: HostRef::new(Store::new(&engine.borrow())), }) } + +#[repr(C)] +pub struct wasmtime_interrupt_handle_t { + handle: InterruptHandle, +} + +wasmtime_c_api_macros::declare_own!(wasmtime_interrupt_handle_t); + +#[no_mangle] +pub extern "C" fn wasmtime_interrupt_handle_new( + store: &wasm_store_t, +) -> Option> { + Some(Box::new(wasmtime_interrupt_handle_t { + handle: store.store.borrow().interrupt_handle().ok()?, + })) +} + +#[no_mangle] +pub extern "C" fn wasmtime_interrupt_handle_interrupt(handle: &wasmtime_interrupt_handle_t) { + handle.handle.interrupt(); +} diff --git a/crates/environ/src/cranelift.rs b/crates/environ/src/cranelift.rs index 7d78024a41ad..0007adae8b65 100644 --- a/crates/environ/src/cranelift.rs +++ b/crates/environ/src/cranelift.rs @@ -1,5 +1,84 @@ //! Support for compiling with Cranelift. +// # How does wasm 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 %rsp +// 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 %rsp, 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 wasm 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 %rsp 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() - 1`. We reserve +// `usize::max_value()` for "the stack limit isn't set yet" and so -2 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. + use crate::address_map::{FunctionAddressMap, InstructionAddressMap}; use crate::cache::{ModuleCacheDataTupleType, ModuleCacheEntry}; use crate::compilation::{ @@ -7,7 +86,7 @@ use crate::compilation::{ }; use crate::func_environ::{get_func_name, FuncEnvironment}; use crate::{CacheConfig, FunctionBodyData, ModuleLocal, ModuleTranslation, Tunables}; -use cranelift_codegen::ir::{self, ExternalName}; +use cranelift_codegen::ir::{self, ArgumentPurpose, ExternalName, InstBuilder}; use cranelift_codegen::print_errors::pretty_error; use cranelift_codegen::{binemit, isa, Context}; use cranelift_entity::PrimaryMap; @@ -208,12 +287,45 @@ fn compile(env: CompileEnv<'_>) -> Result = Vec::new(); diff --git a/crates/environ/src/func_environ.rs b/crates/environ/src/func_environ.rs index 572d5adb74f0..5b92f422a03f 100644 --- a/crates/environ/src/func_environ.rs +++ b/crates/environ/src/func_environ.rs @@ -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, /// 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,35 @@ 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()), + ); + let interrupted_sentinel = pos.ins().iconst(pointer_type, i64::from(INTERRUPTED)); + let cmp = pos + .ins() + .icmp(IntCC::Equal, interrupt, interrupted_sentinel); + pos.ins().trapnz(cmp, ir::TrapCode::Interrupt); + Ok(()) + } } diff --git a/crates/environ/src/lib.rs b/crates/environ/src/lib.rs index 33858efa7f75..f059795f9d82 100644 --- a/crates/environ/src/lib.rs +++ b/crates/environ/src/lib.rs @@ -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; diff --git a/crates/environ/src/lightbeam.rs b/crates/environ/src/lightbeam.rs index 5e9c8f57c344..b6a5071c1d5d 100644 --- a/crates/environ/src/lightbeam.rs +++ b/crates/environ/src/lightbeam.rs @@ -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); diff --git a/crates/environ/src/module_environ.rs b/crates/environ/src/module_environ.rs index 378d419fd401..118570b12453 100644 --- a/crates/environ/src/module_environ.rs +++ b/crates/environ/src/module_environ.rs @@ -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, } -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. diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index 400b8ceb2b18..d0dc0abfdfb3 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -12,6 +12,9 @@ pub struct Tunables { /// Whether or not to generate DWARF debug information. pub debug_info: bool, + + /// Whether or not loops and function entries will be interruptable, + pub interruptable: bool, } impl Default for Tunables { @@ -44,6 +47,7 @@ impl Default for Tunables { dynamic_memory_offset_guard_size: 0x1_0000, debug_info: false, + interruptable: false, } } } diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs index 13c2b90d6757..ea2f21e0742f 100644 --- a/crates/environ/src/vmoffsets.rs +++ b/crates/environ/src/vmoffsets.rs @@ -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: i32 = (u32::max_value() - 32 * 1024) as i32; + #[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. diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index 26a23b0e2379..b5bca1e8a7ec 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -106,6 +106,7 @@ pub struct Config { debug_info: bool, canonicalize_nans: bool, spectest: usize, + interruptable: bool, } impl Config { @@ -115,7 +116,8 @@ impl Config { cfg.debug_info(self.debug_info) .cranelift_nan_canonicalization(self.canonicalize_nans) .cranelift_debug_verifier(self.debug_verifier) - .cranelift_opt_level(self.opt_level.to_wasmtime()); + .cranelift_opt_level(self.opt_level.to_wasmtime()) + .interruptable(self.interruptable); return cfg; } } diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index e25319372558..2a375bb9e616 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -22,6 +22,7 @@ use wasmparser::*; #[derive(Arbitrary, Debug)] struct Swarm { config_debug_info: bool, + config_interruptable: bool, module_new: bool, module_drop: bool, instance_new: bool, @@ -35,6 +36,7 @@ struct Swarm { pub enum ApiCall { ConfigNew, ConfigDebugInfo(bool), + ConfigInterruptable(bool), EngineNew, StoreNew, ModuleNew { id: usize, wasm: super::WasmOptTtf }, @@ -163,9 +165,10 @@ impl Arbitrary for ApiCalls { // minimum size. arbitrary::size_hint::and( ::size_hint(depth), - // `arbitrary_config` uses two bools when - // `swarm.config_debug_info` is true. - <(bool, bool) as Arbitrary>::size_hint(depth), + // `arbitrary_config` uses four bools: + // 2 when `swarm.config_debug_info` is true + // 2 when `swarm.config_interruptable` is true + <(bool, bool, bool, bool) as Arbitrary>::size_hint(depth), ), // We can generate arbitrary `WasmOptTtf` instances, which have // no upper bound on the number of bytes they consume. This sets @@ -187,6 +190,10 @@ fn arbitrary_config( calls.push(ConfigDebugInfo(bool::arbitrary(input)?)); } + if swarm.config_interruptable && bool::arbitrary(input)? { + calls.push(ConfigInterruptable(bool::arbitrary(input)?)); + } + // TODO: flags, features, and compilation strategy. Ok(()) diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index b842ab2d0fc5..29736ca195c7 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -285,6 +285,11 @@ pub fn make_api_calls(api: crate::generators::api::ApiCalls) { config.as_mut().unwrap().debug_info(b); } + ApiCall::ConfigInterruptable(b) => { + log::trace!("enabling interruption"); + config.as_mut().unwrap().interruptable(b); + } + ApiCall::EngineNew => { log::trace!("creating engine"); assert!(engine.is_none()); diff --git a/crates/jit/src/compiler.rs b/crates/jit/src/compiler.rs index 9e1e309eb381..d0307fc32c1a 100644 --- a/crates/jit/src/compiler.rs +++ b/crates/jit/src/compiler.rs @@ -9,6 +9,7 @@ use cranelift_codegen::Context; use cranelift_codegen::{binemit, ir}; use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; use std::collections::HashMap; +use std::sync::Arc; use wasmtime_debug::{emit_debugsections_image, DebugInfoData}; use wasmtime_environ::entity::{EntityRef, PrimaryMap}; use wasmtime_environ::isa::{TargetFrontendConfig, TargetIsa}; @@ -19,7 +20,8 @@ use wasmtime_environ::{ Relocations, Traps, Tunables, VMOffsets, }; use wasmtime_runtime::{ - InstantiationError, SignatureRegistry, VMFunctionBody, VMSharedSignatureIndex, VMTrampoline, + InstantiationError, SignatureRegistry, VMFunctionBody, VMInterrupts, VMSharedSignatureIndex, + VMTrampoline, }; /// Select which kind of compilation to use. @@ -51,6 +53,7 @@ pub struct Compiler { strategy: CompilationStrategy, cache_config: CacheConfig, tunables: Tunables, + interrupts: Arc, } impl Compiler { @@ -68,6 +71,7 @@ impl Compiler { strategy, cache_config, tunables, + interrupts: Arc::new(VMInterrupts::default()), } } } @@ -95,6 +99,11 @@ impl Compiler { &self.tunables } + /// Return the handle by which to interrupt instances + pub fn interrupts(&self) -> &Arc { + &self.interrupts + } + /// Compile the given function bodies. pub(crate) fn compile<'data>( &mut self, diff --git a/crates/jit/src/instantiate.rs b/crates/jit/src/instantiate.rs index d8d92089dbd5..c7bcb73df686 100644 --- a/crates/jit/src/instantiate.rs +++ b/crates/jit/src/instantiate.rs @@ -21,6 +21,7 @@ use wasmtime_environ::{ ModuleEnvironment, Traps, }; use wasmtime_profiling::ProfilingAgent; +use wasmtime_runtime::VMInterrupts; use wasmtime_runtime::{ GdbJitImageRegistration, InstanceHandle, InstantiationError, RuntimeMemoryCreator, SignatureRegistry, VMFunctionBody, VMSharedSignatureIndex, VMTrampoline, @@ -138,6 +139,7 @@ pub struct CompiledModule { dbg_jit_registration: Option>, traps: Traps, address_transform: ModuleAddressMap, + interrupts: Arc, } impl CompiledModule { @@ -162,6 +164,7 @@ impl CompiledModule { raw.dbg_jit_registration, raw.traps, raw.address_transform, + compiler.interrupts().clone(), )) } @@ -175,6 +178,7 @@ impl CompiledModule { dbg_jit_registration: Option, traps: Traps, address_transform: ModuleAddressMap, + interrupts: Arc, ) -> Self { Self { module: Arc::new(module), @@ -185,6 +189,7 @@ impl CompiledModule { dbg_jit_registration: dbg_jit_registration.map(Rc::new), traps, address_transform, + interrupts, } } @@ -203,6 +208,7 @@ impl CompiledModule { resolver: &mut dyn Resolver, sig_registry: &SignatureRegistry, mem_creator: Option<&dyn RuntimeMemoryCreator>, + max_wasm_stack: usize, host_state: Box, ) -> Result { let data_initializers = self @@ -225,6 +231,8 @@ impl CompiledModule { self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)), is_bulk_memory, host_state, + self.interrupts.clone(), + max_wasm_stack, ) } diff --git a/crates/jit/src/link.rs b/crates/jit/src/link.rs index 824c35ced6a9..770d61ea7470 100644 --- a/crates/jit/src/link.rs +++ b/crates/jit/src/link.rs @@ -54,7 +54,6 @@ fn apply_reloc( FloorF64 => wasmtime_f64_floor as usize, TruncF64 => wasmtime_f64_trunc as usize, NearestF64 => wasmtime_f64_nearest as usize, - Probestack => PROBESTACK as usize, other => panic!("unexpected libcall: {}", other), } } @@ -121,38 +120,3 @@ fn apply_reloc( _ => panic!("unsupported reloc kind"), } } - -// A declaration for the stack probe function in Rust's standard library, for -// catching callstack overflow. -cfg_if::cfg_if! { - if #[cfg(all( - target_os = "windows", - target_env = "msvc", - target_pointer_width = "64" - ))] { - extern "C" { - pub fn __chkstk(); - } - const PROBESTACK: unsafe extern "C" fn() = __chkstk; - } else if #[cfg(all(target_os = "windows", target_env = "gnu"))] { - extern "C" { - // ___chkstk (note the triple underscore) is implemented in compiler-builtins/src/x86_64.rs - // by the Rust compiler for the MinGW target - #[cfg(all(target_os = "windows", target_env = "gnu"))] - pub fn ___chkstk(); - } - const PROBESTACK: unsafe extern "C" fn() = ___chkstk; - } else if #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))] { - // As per - // https://github.com/rust-lang/compiler-builtins/blob/cae3e6ea23739166504f9f9fb50ec070097979d4/src/probestack.rs#L39, - // LLVM only has stack-probe support on x86-64 and x86. Thus, on any other CPU - // architecture, we simply use an empty stack-probe function. - extern "C" fn empty_probestack() {} - const PROBESTACK: unsafe extern "C" fn() = empty_probestack; - } else { - extern "C" { - pub fn __rust_probestack(); - } - static PROBESTACK: unsafe extern "C" fn() = __rust_probestack; - } -} diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs index dc5a6698c7f6..ef5e176600f8 100644 --- a/crates/runtime/src/instance.rs +++ b/crates/runtime/src/instance.rs @@ -11,8 +11,8 @@ use crate::traphandlers; use crate::traphandlers::{catch_traps, Trap}; use crate::vmcontext::{ VMBuiltinFunctionsArray, VMCallerCheckedAnyfunc, VMContext, VMFunctionBody, VMFunctionImport, - VMGlobalDefinition, VMGlobalImport, VMMemoryDefinition, VMMemoryImport, VMSharedSignatureIndex, - VMTableDefinition, VMTableImport, VMTrampoline, + VMGlobalDefinition, VMGlobalImport, VMInterrupts, VMMemoryDefinition, VMMemoryImport, + VMSharedSignatureIndex, VMTableDefinition, VMTableImport, VMTrampoline, }; use crate::{ExportFunction, ExportGlobal, ExportMemory, ExportTable}; use memoffset::offset_of; @@ -110,6 +110,10 @@ pub(crate) struct Instance { /// Handler run when `SIGBUS`, `SIGFPE`, `SIGILL`, or `SIGSEGV` are caught by the instance thread. pub(crate) signal_handler: Cell>>, + /// Externally allocated data indicating how this instance will be + /// interrupted. + pub(crate) interrupts: Arc, + /// Additional context used by compiled wasm code. This field is last, and /// represents a dynamically-sized array that extends beyond the nominal /// end of the struct (similar to a flexible array member). @@ -275,6 +279,11 @@ impl Instance { unsafe { self.vmctx_plus_offset(self.offsets.vmctx_builtin_functions_begin()) } } + /// Return a pointer to the interrupts structure + pub fn interrupts(&self) -> *mut *const VMInterrupts { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_interrupts()) } + } + /// Return a reference to the vmctx used by compiled wasm code. pub fn vmctx(&self) -> &VMContext { &self.vmctx @@ -377,17 +386,21 @@ impl Instance { } /// Invoke the WebAssembly start function of the instance, if one is present. - fn invoke_start_function(&self) -> Result<(), InstantiationError> { + fn invoke_start_function(&self, max_wasm_stack: usize) -> Result<(), InstantiationError> { let start_index = match self.module.start_func { Some(idx) => idx, None => return Ok(()), }; - self.invoke_function_index(start_index) + self.invoke_function_index(start_index, max_wasm_stack) .map_err(InstantiationError::StartTrap) } - fn invoke_function_index(&self, callee_index: FuncIndex) -> Result<(), Trap> { + fn invoke_function_index( + &self, + callee_index: FuncIndex, + max_wasm_stack: usize, + ) -> Result<(), Trap> { let (callee_address, callee_vmctx) = match self.module.local.defined_func_index(callee_index) { Some(defined_index) => { @@ -404,17 +417,18 @@ impl Instance { } }; - self.invoke_function(callee_vmctx, callee_address) + self.invoke_function(callee_vmctx, callee_address, max_wasm_stack) } fn invoke_function( &self, callee_vmctx: *mut VMContext, callee_address: *const VMFunctionBody, + max_wasm_stack: usize, ) -> Result<(), Trap> { // Make the call. unsafe { - catch_traps(callee_vmctx, || { + catch_traps(callee_vmctx, max_wasm_stack, || { mem::transmute::< *const VMFunctionBody, unsafe extern "C" fn(*mut VMContext, *mut VMContext), @@ -869,6 +883,8 @@ impl InstanceHandle { dbg_jit_registration: Option>, is_bulk_memory: bool, host_state: Box, + interrupts: Arc, + max_wasm_stack: usize, ) -> Result { let tables = create_tables(&module); let memories = create_memories(&module, mem_creator.unwrap_or(&DefaultMemoryCreator {}))?; @@ -906,6 +922,7 @@ impl InstanceHandle { dbg_jit_registration, host_state, signal_handler: Cell::new(None), + interrupts, vmctx: VMContext {}, }; let layout = instance.alloc_layout(); @@ -964,6 +981,7 @@ impl InstanceHandle { instance.builtin_functions_ptr() as *mut VMBuiltinFunctionsArray, VMBuiltinFunctionsArray::initialized(), ); + *instance.interrupts() = &*instance.interrupts; // Check initializer bounds before initializing anything. Only do this // when bulk memory is disabled, since the bulk memory proposal changes @@ -986,7 +1004,7 @@ impl InstanceHandle { // The WebAssembly spec specifies that the start function is // invoked automatically at instantiation time. - instance.invoke_start_function()?; + instance.invoke_start_function(max_wasm_stack)?; Ok(handle) } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index b89023a9621a..94816f6f132b 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -47,8 +47,8 @@ pub use crate::traphandlers::resume_panic; pub use crate::traphandlers::{catch_traps, raise_lib_trap, raise_user_trap, Trap}; pub use crate::vmcontext::{ VMCallerCheckedAnyfunc, VMContext, VMFunctionBody, VMFunctionImport, VMGlobalDefinition, - VMGlobalImport, VMInvokeArgument, VMMemoryDefinition, VMMemoryImport, VMSharedSignatureIndex, - VMTableDefinition, VMTableImport, VMTrampoline, + VMGlobalImport, VMInterrupts, VMInvokeArgument, VMMemoryDefinition, VMMemoryImport, + VMSharedSignatureIndex, VMTableDefinition, VMTableImport, VMTrampoline, }; /// Version number of this crate. diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index b37cd46e712e..d0c66924decb 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -2,13 +2,14 @@ //! signalhandling mechanisms. use crate::instance::{InstanceHandle, SignalHandler}; -use crate::vmcontext::VMContext; +use crate::VMContext; use backtrace::Backtrace; use std::any::Any; use std::cell::Cell; use std::error::Error; use std::io; use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::sync::Once; use wasmtime_environ::ir; @@ -38,15 +39,10 @@ cfg_if::cfg_if! { // SA_SIGINFO gives us access to information like the program // counter from where the fault happened. // - // SA_ONSTACK allows us to handle signals on an alternate stack, - // so that the handler can run in response to running out of - // stack space on the main stack. Rust installs an alternate - // stack with sigaltstack, so we rely on that. - // // SA_NODEFER allows us to reenter the signal handler if we // crash while handling the signal, and fall through to the // Breakpad handler by testing handlingSegFault. - handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK; + handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER; handler.sa_sigaction = trap_handler as usize; libc::sigemptyset(&mut handler.sa_mask); if libc::sigaction(signal, &handler, slot.as_mut_ptr()) != 0 { @@ -104,7 +100,6 @@ cfg_if::cfg_if! { // out what to do based on the result of the trap handling. let jmp_buf = info.handle_trap( get_pc(context), - false, |handler| handler(signum, siginfo, context), ); @@ -198,7 +193,6 @@ cfg_if::cfg_if! { let record = &*(*exception_info).ExceptionRecord; if record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION && record.ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION && - record.ExceptionCode != EXCEPTION_STACK_OVERFLOW && record.ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO && record.ExceptionCode != EXCEPTION_INT_OVERFLOW { @@ -226,7 +220,6 @@ cfg_if::cfg_if! { }; let jmp_buf = info.handle_trap( (*(*exception_info).ContextRecord).Rip as *const u8, - record.ExceptionCode == EXCEPTION_STACK_OVERFLOW, |handler| handler(exception_info), ); if jmp_buf.is_null() { @@ -302,22 +295,6 @@ pub unsafe fn resume_panic(payload: Box) -> ! { tls::with(|info| info.unwrap().unwind_with(UnwindReason::Panic(payload))) } -#[cfg(target_os = "windows")] -fn reset_guard_page() { - extern "C" { - fn _resetstkoflw() -> winapi::ctypes::c_int; - } - - // We need to restore guard page under stack to handle future stack overflows properly. - // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/resetstkoflw?view=vs-2019 - if unsafe { _resetstkoflw() } == 0 { - panic!("failed to restore stack guard page"); - } -} - -#[cfg(not(target_os = "windows"))] -fn reset_guard_page() {} - /// Stores trace message with backtrace. #[derive(Debug)] pub enum Trap { @@ -330,6 +307,10 @@ pub enum Trap { pc: usize, /// Native stack backtrace at the time the trap occurred backtrace: Backtrace, + /// An indicator for whether this may have been a trap generated from an + /// interrupt, used for switching what would otherwise be a stack + /// overflow trap to be an interrupt trap. + maybe_interrupted: bool, }, /// A trap raised from a wasm libcall @@ -372,15 +353,15 @@ impl Trap { /// returning them as a `Result`. /// /// Highly unsafe since `closure` won't have any dtors run. -pub unsafe fn catch_traps(vmctx: *mut VMContext, mut closure: F) -> Result<(), Trap> +pub unsafe fn catch_traps( + vmctx: *mut VMContext, + max_wasm_stack: usize, + mut closure: F, +) -> Result<(), Trap> where F: FnMut(), { - // Ensure that we have our sigaltstack installed. - #[cfg(unix)] - setup_unix_sigaltstack()?; - - return CallThreadState::new(vmctx).with(|cx| { + return CallThreadState::new(vmctx).with(max_wasm_stack, |cx| { RegisterSetjmp( cx.jmp_buf.as_ptr(), call_closure::, @@ -401,7 +382,6 @@ where pub struct CallThreadState { unwind: Cell, jmp_buf: Cell<*const u8>, - reset_guard_page: Cell, prev: Option<*const CallThreadState>, vmctx: *mut VMContext, handling_trap: Cell, @@ -421,15 +401,19 @@ impl CallThreadState { unwind: Cell::new(UnwindReason::None), vmctx, jmp_buf: Cell::new(ptr::null()), - reset_guard_page: Cell::new(false), prev: None, handling_trap: Cell::new(false), } } - fn with(mut self, closure: impl FnOnce(&CallThreadState) -> i32) -> Result<(), Trap> { + fn with( + mut self, + max_wasm_stack: usize, + closure: impl FnOnce(&CallThreadState) -> i32, + ) -> Result<(), Trap> { tls::with(|prev| { self.prev = prev.map(|p| p as *const _); + let _reset = self.update_stack_limit(max_wasm_stack)?; let ret = tls::set(&self, || closure(&self)); match self.unwind.replace(UnwindReason::None) { UnwindReason::None => { @@ -443,7 +427,15 @@ impl CallThreadState { UnwindReason::LibTrap(trap) => Err(trap), UnwindReason::JitTrap { backtrace, pc } => { debug_assert_eq!(ret, 0); - Err(Trap::Jit { pc, backtrace }) + let maybe_interrupted = unsafe { + (*self.vmctx).instance().interrupts.stack_limit.load(SeqCst) + == wasmtime_environ::INTERRUPTED as usize + }; + Err(Trap::Jit { + pc, + backtrace, + maybe_interrupted, + }) } UnwindReason::Panic(panic) => { debug_assert_eq!(ret, 0); @@ -453,6 +445,87 @@ impl CallThreadState { }) } + /// Checks and/or initializes the wasm native call stack limit. + /// + /// This function will inspect the current state of the stack and calling + /// context to determine which of three buckets we're in: + /// + /// 1. We are the first wasm call on the stack. This means that we need to + /// set up a stack limit where beyond which if the native wasm stack + /// pointer goes beyond forces a trap. For now we simply reserve an + /// arbitrary chunk of bytes (1 MB from roughly the current native stack + /// pointer). This logic will likely get tweaked over time. + /// + /// 2. We aren't the first wasm call on the stack. In this scenario the wasm + /// stack limit is already configured. This case of wasm -> host -> wasm + /// we assume that the native stack consumed by the host is accounted for + /// in the initial stack limit calculation. That means that in this + /// scenario we do nothing. + /// + /// 3. We were previously interrupted. In this case we consume the interrupt + /// here and return a trap, clearing the interrupt and allowing the next + /// wasm call to proceed. + /// + /// The return value here is a trap for case 3, a noop destructor in case 2, + /// and a meaningful destructor in case 1 + /// + /// For more information about interrupts and stack limits see + /// `crates/environ/src/cranelift.rs`. + /// + /// Note that this function must be called with `self` on the stack, not the + /// heap/etc. + fn update_stack_limit(&self, max_wasm_stack: usize) -> Result { + // Make an "educated guess" to figure out where the wasm sp value should + // start trapping if it drops below. + let wasm_stack_limit = self as *const _ as usize - max_wasm_stack; + + let interrupts = unsafe { &**(&*self.vmctx).instance().interrupts() }; + let reset_stack_limit = match interrupts.stack_limit.compare_exchange( + usize::max_value(), + wasm_stack_limit, + SeqCst, + SeqCst, + ) { + Ok(_) => { + // We're the first wasm on the stack so we've now reserved the + // `max_wasm_stack` bytes of native stack space for wasm. + // Nothing left to do here now except reset back when we're + // done. + true + } + Err(n) if n == wasmtime_environ::INTERRUPTED as usize => { + // This means that an interrupt happened before we actually + // called this function, which means that we're now + // considered interrupted. Be sure to consume this interrupt + // as part of this process too. + interrupts.stack_limit.store(usize::max_value(), SeqCst); + return Err(Trap::Wasm { + trap_code: ir::TrapCode::Interrupt, + backtrace: Backtrace::new_unresolved(), + }); + } + Err(_) => { + // The stack limit was previously set by a previous wasm + // call on the stack. We leave the original stack limit for + // wasm in place in that case, and don't reset the stack + // limit when we're done. + false + } + }; + + struct Reset<'a>(bool, &'a AtomicUsize); + + impl Drop for Reset<'_> { + fn drop(&mut self) { + if self.0 { + self.1.store(usize::max_value(), SeqCst); + } + } + } + + Ok(Reset(reset_stack_limit, &interrupts.stack_limit)) + } + fn any_instance(&self, func: impl Fn(&InstanceHandle) -> bool) -> bool { unsafe { if func(&InstanceHandle::from_vmctx(self.vmctx)) { @@ -475,8 +548,6 @@ impl CallThreadState { /// Trap handler using our thread-local state. /// /// * `pc` - the program counter the trap happened at - /// * `reset_guard_page` - whether or not to reset the guard page, - /// currently Windows specific /// * `call_handler` - a closure used to invoke the platform-specific /// signal handler for each instance, if available. /// @@ -492,7 +563,6 @@ impl CallThreadState { fn handle_trap( &self, pc: *const u8, - reset_guard_page: bool, call_handler: impl Fn(&SignalHandler) -> bool, ) -> *const u8 { // If we hit a fault while handling a previous trap, that's quite bad, @@ -532,7 +602,6 @@ impl CallThreadState { return ptr::null(); } let backtrace = Backtrace::new_unresolved(); - self.reset_guard_page.set(reset_guard_page); self.unwind.replace(UnwindReason::JitTrap { backtrace, pc: pc as usize, @@ -542,14 +611,6 @@ impl CallThreadState { } } -impl Drop for CallThreadState { - fn drop(&mut self) { - if self.reset_guard_page.get() { - reset_guard_page(); - } - } -} - // A private inner module for managing the TLS state that we require across // calls in wasm. The WebAssembly code is called from C++ and then a trap may // happen which requires us to read some contextual state to figure out what to @@ -589,112 +650,3 @@ 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. Override it by creating -/// and registering our own alternate stack that is large enough and has a guard -/// page. -#[cfg(unix)] -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. - 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. - const MIN_STACK_SIZE: usize = 16 * 4096; - - enum Tls { - None, - Allocated { - mmap_ptr: *mut libc::c_void, - mmap_size: usize, - }, - BigEnough, - } - - return TLS.with(|slot| unsafe { - let mut slot = slot.borrow_mut(); - match *slot { - Tls::None => {} - // already checked - _ => return Ok(()), - } - - // 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(()); - } - - // ... 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 register 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 { - // Deallocate the stack memory. - let r = libc::munmap(ptr, size); - debug_assert_eq!(r, 0, "munmap failed during thread shutdown"); - } - } - } -} diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs index c986c217182d..cad3b9eba7cd 100644 --- a/crates/runtime/src/vmcontext.rs +++ b/crates/runtime/src/vmcontext.rs @@ -3,6 +3,7 @@ use crate::instance::Instance; use std::any::Any; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::{ptr, u32}; use wasmtime_environ::BuiltinFunctionIndex; @@ -612,6 +613,52 @@ impl VMInvokeArgument { } } +/// Structure used to control interrupting wasm code, currently with only one +/// atomic flag internally used. +#[derive(Debug)] +#[repr(C)] +pub struct VMInterrupts { + /// Current stack limit of the wasm module. + /// + /// This is used to control both stack overflow as well as interrupting wasm + /// modules. For more information see `crates/environ/src/cranelift.rs`. + pub stack_limit: AtomicUsize, +} + +impl VMInterrupts { + /// Flag that an interrupt should occur + pub fn interrupt(&self) { + self.stack_limit + .store(wasmtime_environ::INTERRUPTED as usize, SeqCst); + } +} + +impl Default for VMInterrupts { + fn default() -> VMInterrupts { + VMInterrupts { + stack_limit: AtomicUsize::new(usize::max_value()), + } + } +} + +#[cfg(test)] +mod test_vminterrupts { + use super::VMInterrupts; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vminterrupts_interrupted_offset() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + offset_of!(VMInterrupts, stack_limit), + usize::from(offsets.vminterrupts_stack_limit()) + ); + } +} + /// The VM "context", which is pointed to by the `vmctx` arg in Cranelift. /// This has information about globals, memories, tables, and other runtime /// state associated with the current instance. diff --git a/crates/wiggle/src/lib.rs b/crates/wiggle/src/lib.rs index d91c60128cca..2f7d6c7773b9 100644 --- a/crates/wiggle/src/lib.rs +++ b/crates/wiggle/src/lib.rs @@ -401,7 +401,7 @@ impl<'a, T> GuestPtr<'a, [T]> { /// trait documentation. /// /// For safety against overlapping mutable borrows, the user must use the - /// same `GuestBorrows` to create all *mut str or *mut [T] that are alive + /// same `GuestBorrows` to create all `*mut str` or `*mut [T]` that are alive /// at the same time. pub fn as_raw(&self, bc: &mut GuestBorrows) -> Result<*mut [T], GuestError> where @@ -503,8 +503,8 @@ impl<'a> GuestPtr<'a, str> { /// trait documentation. /// /// For safety against overlapping mutable borrows, the user must use the - /// same `GuestBorrows` to create all *mut str or *mut [T] that are alive - /// at the same time. + /// same `GuestBorrows` to create all `*mut str` or `*mut [T]` that are + /// alive at the same time. pub fn as_raw(&self, bc: &mut GuestBorrows) -> Result<*mut str, GuestError> { let ptr = self .mem diff --git a/examples/interrupt.c b/examples/interrupt.c new file mode 100644 index 000000000000..2a4f5cfbd289 --- /dev/null +++ b/examples/interrupt.c @@ -0,0 +1,141 @@ +/* +Example of instantiating of the WebAssembly module and invoking its exported +function. + +You can compile and run this example on Linux with: + + cargo build --release -p wasmtime + cc examples/interrupt.c \ + -I crates/c-api/include \ + -I crates/c-api/wasm-c-api/include \ + target/release/libwasmtime.a \ + -lpthread -ldl -lm \ + -o interrupt + ./interrupt + +Note that on Windows and macOS the command will be similar, but you'll need +to tweak the `-lpthread` and such annotations as well as the name of the +`libwasmtime.a` file on Windows. +*/ + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +static void spawn_interrupt(wasmtime_interrupt_handle_t *handle) { + wasmtime_interrupt_handle_interrupt(handle); + wasmtime_interrupt_handle_delete(handle); +} +#else +#include +#include + +static void* helper(void *_handle) { + wasmtime_interrupt_handle_t *handle = _handle; + struct timespec sleep_dur; + sleep_dur.tv_sec = 1; + sleep_dur.tv_nsec = 0; + nanosleep(&sleep_dur, NULL); + printf("Sending an interrupt\n"); + wasmtime_interrupt_handle_interrupt(handle); + wasmtime_interrupt_handle_delete(handle); +} + +static void spawn_interrupt(wasmtime_interrupt_handle_t *handle) { + pthread_t child; + int rc = pthread_create(&child, NULL, helper, handle); + assert(rc == 0); +} +#endif + +static void exit_with_error(const char *message, wasmtime_error_t *error, wasm_trap_t *trap); + +int main() { + // Create a `wasm_store_t` with interrupts enabled + wasm_config_t *config = wasm_config_new(); + assert(config != NULL); + wasmtime_config_interruptable_set(config, true); + wasm_engine_t *engine = wasm_engine_new_with_config(config); + assert(engine != NULL); + wasm_store_t *store = wasm_store_new(engine); + assert(store != NULL); + + // Create our interrupt handle we'll use later + wasmtime_interrupt_handle_t *handle = wasmtime_interrupt_handle_new(store); + assert(handle != NULL); + + // Read our input file, which in this case is a wasm text file. + FILE* file = fopen("examples/interrupt.wat", "r"); + assert(file != NULL); + fseek(file, 0L, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0L, SEEK_SET); + wasm_byte_vec_t wat; + wasm_byte_vec_new_uninitialized(&wat, file_size); + assert(fread(wat.data, file_size, 1, file) == 1); + fclose(file); + + // Parse the wat into the binary wasm format + wasm_byte_vec_t wasm; + wasmtime_error_t *error = wasmtime_wat2wasm(&wat, &wasm); + if (error != NULL) + exit_with_error("failed to parse wat", error, NULL); + wasm_byte_vec_delete(&wat); + + // Now that we've got our binary webassembly we can compile our module. + wasm_module_t *module = NULL; + wasm_trap_t *trap = NULL; + wasm_instance_t *instance = NULL; + error = wasmtime_module_new(store, &wasm, &module); + wasm_byte_vec_delete(&wasm); + if (error != NULL) + exit_with_error("failed to compile module", error, NULL); + error = wasmtime_instance_new(module, NULL, 0, &instance, &trap); + if (instance == NULL) + exit_with_error("failed to instantiate", error, trap); + + // Lookup our `run` export function + wasm_extern_vec_t externs; + wasm_instance_exports(instance, &externs); + assert(externs.size == 1); + wasm_func_t *run = wasm_extern_as_func(externs.data[0]); + assert(run != NULL); + + // Spawn a thread to send us an interrupt after a period of time. + spawn_interrupt(handle); + + // And call it! + printf("Entering infinite loop...\n"); + error = wasmtime_func_call(run, NULL, 0, NULL, 0, &trap); + assert(error == NULL); + assert(trap != NULL); + printf("Got a trap!...\n"); + + // `trap` can be inspected here to see the trap message has an interrupt in it + + wasm_trap_delete(trap); + wasm_extern_vec_delete(&externs); + wasm_instance_delete(instance); + wasm_module_delete(module); + wasm_store_delete(store); + wasm_engine_delete(engine); + return 0; +} + +static void exit_with_error(const char *message, wasmtime_error_t *error, wasm_trap_t *trap) { + fprintf(stderr, "error: %s\n", message); + wasm_byte_vec_t error_message; + if (error != NULL) { + wasmtime_error_message(error, &error_message); + wasmtime_error_delete(error); + } else { + wasm_trap_message(trap, &error_message); + wasm_trap_delete(trap); + } + fprintf(stderr, "%.*s\n", (int) error_message.size, error_message.data); + wasm_byte_vec_delete(&error_message); + exit(1); +} diff --git a/examples/interrupt.rs b/examples/interrupt.rs new file mode 100644 index 000000000000..7f724668ffb0 --- /dev/null +++ b/examples/interrupt.rs @@ -0,0 +1,39 @@ +//! Small example of how you can interrupt the execution of a wasm module to +//! ensure that it doesn't run for too long. + +// You can execute this example with `cargo run --example interrupt` + +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::from_file(&store, "examples/interrupt.wat")?; + let instance = Instance::new(&module, &[])?; + let run = instance + .get_export("run") + .and_then(|e| e.func()) + .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)); + println!("Interrupting!"); + interrupt_handle.interrupt(); + }); + + println!("Entering infinite loop ..."); + let trap = run().unwrap_err(); + + println!("trap received..."); + assert!(trap.message().contains("wasm trap: interrupt")); + + Ok(()) +} diff --git a/examples/interrupt.wat b/examples/interrupt.wat new file mode 100644 index 000000000000..b7d8cad33fb5 --- /dev/null +++ b/examples/interrupt.wat @@ -0,0 +1,6 @@ +(module + (func (export "run") + (loop + br 0) + ) +) From aa839cb90723391f87aff0a59b87f08c40dfab71 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 17 Apr 2020 14:24:56 -0700 Subject: [PATCH 02/13] Update comment about magical interrupt value --- crates/environ/src/cranelift.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/environ/src/cranelift.rs b/crates/environ/src/cranelift.rs index 0007adae8b65..2d76e9ae5b7e 100644 --- a/crates/environ/src/cranelift.rs +++ b/crates/environ/src/cranelift.rs @@ -73,11 +73,17 @@ // 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() - 1`. We reserve -// `usize::max_value()` for "the stack limit isn't set yet" and so -2 is +// 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. +// 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}; From a35fa642ac550ca7483ab0ff92047764457c35e9 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 17 Apr 2020 15:03:16 -0700 Subject: [PATCH 03/13] Store stack limit as a global value, not a closure --- cranelift/codegen/src/ir/function.rs | 22 ++++++---------- cranelift/codegen/src/isa/x86/abi.rs | 32 +++++++++++++++++++++-- crates/environ/src/cranelift.rs | 39 +++++++++++++++------------- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/cranelift/codegen/src/ir/function.rs b/cranelift/codegen/src/ir/function.rs index e27a9be2c823..892af400afb4 100644 --- a/cranelift/codegen/src/ir/function.rs +++ b/cranelift/codegen/src/ir/function.rs @@ -4,7 +4,6 @@ //! instructions. use crate::binemit::CodeOffset; -use crate::cursor::EncCursor; use crate::entity::{PrimaryMap, SecondaryMap}; use crate::ir; use crate::ir::{ @@ -97,17 +96,12 @@ pub struct Function { /// This is used for some ABIs to generate unwind information. pub epilogues_start: Vec, - /// An optional closure which calculates the stack limit for this function - /// from the arguments of the function. - /// - /// When configured a stack check will be emitted in the prologue of this - /// function, trapping if the stack check fails. This closure, if specified, - /// can be then used to infer the stack limit from provided arguments as an - /// alternative to using `ArgumentPurpose::StackLimit`. - /// - /// The first argument is the cursor we're encoding the prologue too, and - /// the second argument is a scratch register location if necessary. - pub stack_limit_from_arguments: Option ir::Value>, + /// An optional global value which represents an expression evaluating to + /// the stack limit for this function. This `GlobalValue` will be + /// interpreted in the prologue, if necessary, to insert a stack check to + /// ensure that a trap happens if the stack pointer goes below the + /// threshold specified here. + pub stack_limit: Option, } impl Function { @@ -132,7 +126,7 @@ impl Function { srclocs: SecondaryMap::new(), prologue_end: None, epilogues_start: Vec::new(), - stack_limit_from_arguments: None, + stack_limit: None, } } @@ -154,7 +148,7 @@ impl Function { self.srclocs.clear(); self.prologue_end = None; self.epilogues_start.clear(); - self.stack_limit_from_arguments = None; + self.stack_limit = None; } /// Create a new empty, anonymous function with a Fast calling convention. diff --git a/cranelift/codegen/src/isa/x86/abi.rs b/cranelift/codegen/src/isa/x86/abi.rs index 224305e3be0b..cc4fef98f17a 100644 --- a/cranelift/codegen/src/isa/x86/abi.rs +++ b/cranelift/codegen/src/isa/x86/abi.rs @@ -706,8 +706,8 @@ fn insert_common_prologue( } None => pos .func - .stack_limit_from_arguments - .map(|closure| closure(pos, scratch)), + .stack_limit + .map(|gv| interpret_gv(pos, gv, scratch)), }; if let Some(stack_limit_arg) = stack_limit_arg { insert_stack_check(pos, stack_size, stack_limit_arg); @@ -822,6 +822,34 @@ fn insert_common_prologue( ); } +/// Inserts code necessary to calculate `gv`. +/// +/// Note that this is typically done with `ins().global_value(...)` but that +/// requires legalization to run to encode it, and we're running super late +/// here in the backend where legalization isn't possible. To get around this +/// we manually interpret the `gv` specified and do register allocation for +/// intermediate values. +/// +/// This is an incomplete implementation of loading `GlobalValue` values to get +/// compared to the stack pointer, but currently it serves enough functionality +/// to get this implemented in `wasmtime` itself. This'll likely get expanded a +/// bit over time! +fn interpret_gv(pos: &mut EncCursor, gv: ir::GlobalValue, scratch: ir::ValueLoc) -> ir::Value { + match pos.func.global_values[gv] { + ir::GlobalValueData::VMContext => { + pos.func.special_param(ir::ArgumentPurpose::VMContext) + .expect("no vmcontext parameter found") + } + ir::GlobalValueData::Load { base, offset, global_type, readonly: _ } => { + let base = interpret_gv(pos, base, scratch); + let ret = pos.ins().load(global_type, ir::MemFlags::trusted(), base, offset); + pos.func.locations[ret] = scratch; + return ret; + } + ref other => panic!("global value for stack limit not supported: {}", other), + } +} + /// Insert a check that generates a trap if the stack pointer goes /// below a value in `stack_limit_arg`. fn insert_stack_check(pos: &mut EncCursor, stack_size: i64, stack_limit_arg: ir::Value) { diff --git a/crates/environ/src/cranelift.rs b/crates/environ/src/cranelift.rs index 2d76e9ae5b7e..5438914270ff 100644 --- a/crates/environ/src/cranelift.rs +++ b/crates/environ/src/cranelift.rs @@ -92,12 +92,13 @@ use crate::compilation::{ }; use crate::func_environ::{get_func_name, FuncEnvironment}; use crate::{CacheConfig, FunctionBodyData, ModuleLocal, ModuleTranslation, Tunables}; -use cranelift_codegen::ir::{self, ArgumentPurpose, ExternalName, InstBuilder}; +use cranelift_codegen::ir::{self, ExternalName}; use cranelift_codegen::print_errors::pretty_error; 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 @@ -308,24 +309,26 @@ fn compile(env: CompileEnv<'_>) -> Result Date: Fri, 17 Apr 2020 15:23:58 -0700 Subject: [PATCH 04/13] Run rustfmt --- cranelift/codegen/src/isa/x86/abi.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cranelift/codegen/src/isa/x86/abi.rs b/cranelift/codegen/src/isa/x86/abi.rs index cc4fef98f17a..3e353229c6a9 100644 --- a/cranelift/codegen/src/isa/x86/abi.rs +++ b/cranelift/codegen/src/isa/x86/abi.rs @@ -836,13 +836,20 @@ fn insert_common_prologue( /// bit over time! fn interpret_gv(pos: &mut EncCursor, gv: ir::GlobalValue, scratch: ir::ValueLoc) -> ir::Value { match pos.func.global_values[gv] { - ir::GlobalValueData::VMContext => { - pos.func.special_param(ir::ArgumentPurpose::VMContext) - .expect("no vmcontext parameter found") - } - ir::GlobalValueData::Load { base, offset, global_type, readonly: _ } => { + ir::GlobalValueData::VMContext => pos + .func + .special_param(ir::ArgumentPurpose::VMContext) + .expect("no vmcontext parameter found"), + ir::GlobalValueData::Load { + base, + offset, + global_type, + readonly: _, + } => { let base = interpret_gv(pos, base, scratch); - let ret = pos.ins().load(global_type, ir::MemFlags::trusted(), base, offset); + let ret = pos + .ins() + .load(global_type, ir::MemFlags::trusted(), base, offset); pos.func.locations[ret] = scratch; return ret; } From f6fbe1779a54c0f88d1c30c841a1a369e35818ee Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 07:38:02 -0700 Subject: [PATCH 05/13] Handle review comments --- crates/environ/src/cranelift.rs | 18 +++++++------- crates/environ/src/tunables.rs | 7 +++++- src/commands/run.rs | 18 +++++++++++++- tests/all/cli_tests.rs | 42 +++++++++++++++++++++++++++++++++ tests/wasm/iloop-invoke.wat | 2 ++ tests/wasm/iloop-start.wat | 3 +++ 6 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 tests/wasm/iloop-invoke.wat create mode 100644 tests/wasm/iloop-start.wat diff --git a/crates/environ/src/cranelift.rs b/crates/environ/src/cranelift.rs index 5438914270ff..62ea3d85c360 100644 --- a/crates/environ/src/cranelift.rs +++ b/crates/environ/src/cranelift.rs @@ -1,6 +1,6 @@ //! Support for compiling with Cranelift. -// # How does wasm prevent stack overflow? +// # 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 @@ -15,10 +15,10 @@ // 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 %rsp -// 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 %rsp, and optionally traps. +// 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 @@ -41,7 +41,7 @@ // size of wasm, see the implementation in `traphandlers.rs` in the // `update_stack_limit` function. // -// # How is wasm interrupted? +// # 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 @@ -69,9 +69,9 @@ // 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 %rsp 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. +// 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 diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index d0dc0abfdfb3..13de779ff036 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -13,7 +13,12 @@ pub struct Tunables { /// Whether or not to generate DWARF debug information. pub debug_info: bool, - /// Whether or not loops and function entries will be interruptable, + /// 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, } diff --git a/src/commands/run.rs b/src/commands/run.rs index 95c795d582a4..869db5aaffa7 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -2,6 +2,8 @@ use crate::{init_file_per_thread_logger, CommonOptions}; use anyhow::{bail, Context as _, Result}; +use std::thread; +use std::time::Duration; use std::{ ffi::{OsStr, OsString}, fs::File, @@ -80,6 +82,10 @@ pub struct RunCommand { )] preloads: Vec, + /// Maximum execution time of wasm code before timing out + #[structopt(long = "wasm-timeout")] + wasm_timeout: Option, + // NOTE: this must come last for trailing varargs /// The arguments to pass to the module #[structopt(value_name = "ARGS")] @@ -96,7 +102,10 @@ impl RunCommand { pretty_env_logger::init(); } - let config = self.common.config()?; + let mut config = self.common.config()?; + if self.wasm_timeout.is_some() { + config.interruptable(true); + } let engine = Engine::new(&config); let store = Store::new(&engine); @@ -225,6 +234,13 @@ impl RunCommand { } fn handle_module(&self, store: &Store, module_registry: &ModuleRegistry) -> Result<()> { + if let Some(timeout) = self.wasm_timeout { + let handle = store.interrupt_handle()?; + thread::spawn(move || { + thread::sleep(Duration::from_secs(timeout)); + handle.interrupt(); + }); + } let instance = Self::instantiate_module(store, module_registry, &self.module)?; // If a function to invoke was given, invoke it. diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 9c462d88010c..01f282814cd7 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -122,3 +122,45 @@ fn hello_wasi_snapshot1() -> Result<()> { assert_eq!(stdout, "Hello, world!\n"); Ok(()) } + +#[test] +fn timeout_in_start() -> Result<()> { + let wasm = build_wasm("tests/wasm/iloop-start.wat")?; + let output = run_wasmtime_for_output(&[ + "run", + wasm.path().to_str().unwrap(), + "--wasm-timeout", + "1", + "--disable-cache", + ])?; + assert!(!output.status.success()); + assert_eq!(output.stdout, b""); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("wasm trap: interrupt"), + "bad stderr: {}", + stderr + ); + Ok(()) +} + +#[test] +fn timeout_in_invoke() -> Result<()> { + let wasm = build_wasm("tests/wasm/iloop-invoke.wat")?; + let output = run_wasmtime_for_output(&[ + "run", + wasm.path().to_str().unwrap(), + "--wasm-timeout", + "1", + "--disable-cache", + ])?; + assert!(!output.status.success()); + assert_eq!(output.stdout, b""); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("wasm trap: interrupt"), + "bad stderr: {}", + stderr + ); + Ok(()) +} diff --git a/tests/wasm/iloop-invoke.wat b/tests/wasm/iloop-invoke.wat new file mode 100644 index 000000000000..f297fa0a259c --- /dev/null +++ b/tests/wasm/iloop-invoke.wat @@ -0,0 +1,2 @@ +(module + (func (export "_start") (loop br 0))) diff --git a/tests/wasm/iloop-start.wat b/tests/wasm/iloop-start.wat new file mode 100644 index 000000000000..d1a869149ba8 --- /dev/null +++ b/tests/wasm/iloop-start.wat @@ -0,0 +1,3 @@ +(module + (start 0) + (func (loop br 0))) From d83371ccfacd6b807c402244e9a22af57401dcae Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 07:47:29 -0700 Subject: [PATCH 06/13] Add a comment about SA_ONSTACK --- crates/runtime/src/traphandlers.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index d0c66924decb..35bd369e285f 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -54,6 +54,21 @@ cfg_if::cfg_if! { }; // Allow handling OOB with signals on all architectures + // + // Note that this is overriding the Rust standard library's signal + // handler. The standard library's signal handler, however, only + // serves the purpose of printing out that a stack overflow has + // happened when it happens. Doing so requires the `SA_ONSTACK` + // flag, though, which we're specifically avoiding here because + // we don't want to run on the small sigaltstack, but rather the + // much larger main stack. + // + // The consequence of this is tha programs using Wasmtime which + // overrun the native stack will not print out a nice message + // saying that they're out of stack. Instead they'll simply + // segfault and/or sigbus depending on the platform. Note though + // that this isn't undefined behavior, but the stack protection + // mechanisms in Rust will guarantee a segfault. register(&mut PREV_SIGSEGV, libc::SIGSEGV); // Handle `unreachable` instructions which execute `ud2` right now From 42f4edb92825512e3912416d5709526eece9339e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 07:49:16 -0700 Subject: [PATCH 07/13] Use `usize` for type of `INTERRUPTED` --- crates/environ/src/func_environ.rs | 4 +++- crates/environ/src/vmoffsets.rs | 2 +- crates/runtime/src/traphandlers.rs | 4 ++-- crates/runtime/src/vmcontext.rs | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/environ/src/func_environ.rs b/crates/environ/src/func_environ.rs index 5b92f422a03f..baa252e530ff 100644 --- a/crates/environ/src/func_environ.rs +++ b/crates/environ/src/func_environ.rs @@ -1274,7 +1274,9 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m interrupt_ptr, i32::from(self.offsets.vminterrupts_stack_limit()), ); - let interrupted_sentinel = pos.ins().iconst(pointer_type, i64::from(INTERRUPTED)); + // 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); diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs index ea2f21e0742f..3988198df020 100644 --- a/crates/environ/src/vmoffsets.rs +++ b/crates/environ/src/vmoffsets.rs @@ -29,7 +29,7 @@ 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: i32 = (u32::max_value() - 32 * 1024) as i32; +pub const INTERRUPTED: usize = usize::max_value() - 32 * 1024; #[cfg(target_pointer_width = "32")] fn cast_to_u32(sz: usize) -> u32 { diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 35bd369e285f..b357ad7fc76a 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -444,7 +444,7 @@ impl CallThreadState { debug_assert_eq!(ret, 0); let maybe_interrupted = unsafe { (*self.vmctx).instance().interrupts.stack_limit.load(SeqCst) - == wasmtime_environ::INTERRUPTED as usize + == wasmtime_environ::INTERRUPTED }; Err(Trap::Jit { pc, @@ -508,7 +508,7 @@ impl CallThreadState { // done. true } - Err(n) if n == wasmtime_environ::INTERRUPTED as usize => { + Err(n) if n == wasmtime_environ::INTERRUPTED => { // This means that an interrupt happened before we actually // called this function, which means that we're now // considered interrupted. Be sure to consume this interrupt diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs index cad3b9eba7cd..94dc4080d995 100644 --- a/crates/runtime/src/vmcontext.rs +++ b/crates/runtime/src/vmcontext.rs @@ -629,7 +629,7 @@ impl VMInterrupts { /// Flag that an interrupt should occur pub fn interrupt(&self) { self.stack_limit - .store(wasmtime_environ::INTERRUPTED as usize, SeqCst); + .store(wasmtime_environ::INTERRUPTED, SeqCst); } } From 46029aa72600c9e99e8dc6b2c37b29b5338a6828 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 08:38:27 -0700 Subject: [PATCH 08/13] Parse human-readable durations --- Cargo.lock | 1 + Cargo.toml | 1 + crates/runtime/src/traphandlers.rs | 2 +- src/commands/run.rs | 22 ++++++++++++++++++---- tests/all/cli_tests.rs | 4 ++-- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6e06ebb814a..ad6f5a512aaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2093,6 +2093,7 @@ dependencies = [ "faerie", "file-per-thread-logger", "filecheck", + "humantime", "libc", "more-asserts", "pretty_env_logger", diff --git a/Cargo.toml b/Cargo.toml index 3161417cbc14..d68ed6f44fd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ file-per-thread-logger = "0.1.1" wat = "1.0.10" libc = "0.2.60" rayon = "1.2.1" +humantime = "1.3.0" [dev-dependencies] filecheck = "0.5.0" diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index b357ad7fc76a..4c4af8740355 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -63,7 +63,7 @@ cfg_if::cfg_if! { // we don't want to run on the small sigaltstack, but rather the // much larger main stack. // - // The consequence of this is tha programs using Wasmtime which + // The consequence of this is that programs using Wasmtime which // overrun the native stack will not print out a nice message // saying that they're out of stack. Instead they'll simply // segfault and/or sigbus depending on the platform. Note though diff --git a/src/commands/run.rs b/src/commands/run.rs index 869db5aaffa7..d30785eb68e6 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -41,6 +41,16 @@ fn parse_map_dirs(s: &str) -> Result<(String, String)> { Ok((parts[0].into(), parts[1].into())) } +fn parse_dur(s: &str) -> Result { + // assume an integer without a unit specified is a number of seconds ... + if let Ok(val) = s.parse() { + return Ok(Duration::from_secs(val)); + } + // ... otherwise try to parse it with units such as `3s` or `300ms` + let dur = humantime::parse_duration(s)?; + Ok(dur) +} + /// Runs a WebAssembly module #[derive(StructOpt)] #[structopt(name = "run", setting = AppSettings::TrailingVarArg)] @@ -82,9 +92,13 @@ pub struct RunCommand { )] preloads: Vec, - /// Maximum execution time of wasm code before timing out - #[structopt(long = "wasm-timeout")] - wasm_timeout: Option, + /// Maximum execution time of wasm code before timing out (1, 2s, 100ms, etc) + #[structopt( + long = "wasm-timeout", + value_name = "TIME", + parse(try_from_str = parse_dur), + )] + wasm_timeout: Option, // NOTE: this must come last for trailing varargs /// The arguments to pass to the module @@ -237,7 +251,7 @@ impl RunCommand { if let Some(timeout) = self.wasm_timeout { let handle = store.interrupt_handle()?; thread::spawn(move || { - thread::sleep(Duration::from_secs(timeout)); + thread::sleep(timeout); handle.interrupt(); }); } diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 01f282814cd7..855fab0def52 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -130,7 +130,7 @@ fn timeout_in_start() -> Result<()> { "run", wasm.path().to_str().unwrap(), "--wasm-timeout", - "1", + "1ms", "--disable-cache", ])?; assert!(!output.status.success()); @@ -151,7 +151,7 @@ fn timeout_in_invoke() -> Result<()> { "run", wasm.path().to_str().unwrap(), "--wasm-timeout", - "1", + "1ms", "--disable-cache", ])?; assert!(!output.status.success()); From e954b165a89ca982a9498127dd1bdbf8699767ba Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 08:45:19 -0700 Subject: [PATCH 09/13] Bring back sigaltstack handling Allows libstd to print out stack overflow on failure still. --- crates/runtime/src/traphandlers.rs | 135 +++++++++++++++++++++++++---- tests/host_segfault.rs | 17 ++++ 2 files changed, 136 insertions(+), 16 deletions(-) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index 4c4af8740355..488dc3a0598c 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/crates/runtime/src/traphandlers.rs @@ -39,10 +39,15 @@ cfg_if::cfg_if! { // SA_SIGINFO gives us access to information like the program // counter from where the fault happened. // + // SA_ONSTACK allows us to handle signals on an alternate stack, + // so that the handler can run in response to running out of + // stack space on the main stack. Rust installs an alternate + // stack with sigaltstack, so we rely on that. + // // SA_NODEFER allows us to reenter the signal handler if we // crash while handling the signal, and fall through to the // Breakpad handler by testing handlingSegFault. - handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER; + handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK; handler.sa_sigaction = trap_handler as usize; libc::sigemptyset(&mut handler.sa_mask); if libc::sigaction(signal, &handler, slot.as_mut_ptr()) != 0 { @@ -54,21 +59,6 @@ cfg_if::cfg_if! { }; // Allow handling OOB with signals on all architectures - // - // Note that this is overriding the Rust standard library's signal - // handler. The standard library's signal handler, however, only - // serves the purpose of printing out that a stack overflow has - // happened when it happens. Doing so requires the `SA_ONSTACK` - // flag, though, which we're specifically avoiding here because - // we don't want to run on the small sigaltstack, but rather the - // much larger main stack. - // - // The consequence of this is that programs using Wasmtime which - // overrun the native stack will not print out a nice message - // saying that they're out of stack. Instead they'll simply - // segfault and/or sigbus depending on the platform. Note though - // that this isn't undefined behavior, but the stack protection - // mechanisms in Rust will guarantee a segfault. register(&mut PREV_SIGSEGV, libc::SIGSEGV); // Handle `unreachable` instructions which execute `ud2` right now @@ -376,6 +366,10 @@ pub unsafe fn catch_traps( where F: FnMut(), { + // Ensure that we have our sigaltstack installed. + #[cfg(unix)] + setup_unix_sigaltstack()?; + return CallThreadState::new(vmctx).with(max_wasm_stack, |cx| { RegisterSetjmp( cx.jmp_buf.as_ptr(), @@ -665,3 +659,112 @@ 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. Override it by creating +/// and registering our own alternate stack that is large enough and has a guard +/// page. +#[cfg(unix)] +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. + 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. + const MIN_STACK_SIZE: usize = 16 * 4096; + + enum Tls { + None, + Allocated { + mmap_ptr: *mut libc::c_void, + mmap_size: usize, + }, + BigEnough, + } + + return TLS.with(|slot| unsafe { + let mut slot = slot.borrow_mut(); + match *slot { + Tls::None => {} + // already checked + _ => return Ok(()), + } + + // 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(()); + } + + // ... 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 register 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 { + // Deallocate the stack memory. + let r = libc::munmap(ptr, size); + debug_assert_eq!(r, 0, "munmap failed during thread shutdown"); + } + } + } +} diff --git a/tests/host_segfault.rs b/tests/host_segfault.rs index 127a6d79436e..4728008c42d6 100644 --- a/tests/host_segfault.rs +++ b/tests/host_segfault.rs @@ -24,6 +24,15 @@ fn segfault() -> ! { } } +fn overrun_the_stack() -> usize { + let mut a = [0u8; 1024]; + if a.as_mut_ptr() as usize == 1 { + return 1; + } else { + return a.as_mut_ptr() as usize + overrun_the_stack(); + } +} + fn main() { let tests: &[(&str, fn())] = &[ ("normal segfault", || segfault()), @@ -33,6 +42,12 @@ fn main() { let _instance = Instance::new(&module, &[]).unwrap(); segfault(); }), + ("make instance then overrun the stack", || { + let store = Store::default(); + let module = Module::new(&store, "(module)").unwrap(); + let _instance = Instance::new(&module, &[]).unwrap(); + overrun_the_stack(); + }), ]; match env::var(VAR_NAME) { Ok(s) => { @@ -76,6 +91,8 @@ fn runtest(name: &str) { name, desc ); + } else if name.contains("overrun the stack") { + assert!(stderr.contains("thread 'main' has overflowed its stack")); } else { panic!("\n\nexpected a segfault on `{}`\n{}\n\n", name, desc); } From 4037d577cf7bfc9977c0483cc0fcfe263d32cddc Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 13:51:59 -0700 Subject: [PATCH 10/13] Add parsing and emission of stack limit-via-preamble --- cranelift/codegen/src/ir/entities.rs | 3 ++ cranelift/codegen/src/write.rs | 5 ++ .../filetests/isa/x86/prologue-epilogue.clif | 32 +++++++++++++ cranelift/reader/src/parser.rs | 46 +++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/cranelift/codegen/src/ir/entities.rs b/cranelift/codegen/src/ir/entities.rs index b480722ff4e8..e62947553226 100644 --- a/cranelift/codegen/src/ir/entities.rs +++ b/cranelift/codegen/src/ir/entities.rs @@ -392,6 +392,8 @@ pub enum AnyEntity { Heap(Heap), /// A table. Table(Table), + /// A function's stack limit + StackLimit, } impl fmt::Display for AnyEntity { @@ -409,6 +411,7 @@ impl fmt::Display for AnyEntity { Self::SigRef(r) => r.fmt(f), Self::Heap(r) => r.fmt(f), Self::Table(r) => r.fmt(f), + Self::StackLimit => write!(f, "stack_limit"), } } } diff --git a/cranelift/codegen/src/write.rs b/cranelift/codegen/src/write.rs index 1b3f4de550d4..acf181af2bec 100644 --- a/cranelift/codegen/src/write.rs +++ b/cranelift/codegen/src/write.rs @@ -107,6 +107,11 @@ pub trait FuncWriter { self.write_entity_definition(w, func, cref.into(), cval)?; } + if let Some(limit) = func.stack_limit { + any = true; + self.write_entity_definition(w, func, AnyEntity::StackLimit, &limit)?; + } + Ok(any) } diff --git a/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif b/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif index 3804ad3e77f4..c28dd886ea66 100644 --- a/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif +++ b/cranelift/filetests/filetests/isa/x86/prologue-epilogue.clif @@ -280,3 +280,35 @@ block0(v0: i64): ; nextln: v6 = x86_pop.i64 ; nextln: return v6 ; nextln: } + +function %limit_preamble(i64 vmctx) { + gv0 = vmctx + gv1 = load.i64 notrap aligned gv0 + gv2 = load.i64 notrap aligned gv1+4 + stack_limit = gv2 + ss0 = explicit_slot 20 +block0(v0: i64): + return +} + +; check: function %limit_preamble(i64 vmctx [%rdi], i64 fp [%rbp]) -> i64 fp [%rbp] fast { +; nextln: ss0 = explicit_slot 20, offset -36 +; nextln: ss1 = incoming_arg 16, offset -16 +; nextln: gv0 = vmctx +; nextln: gv1 = load.i64 notrap aligned gv0 +; nextln: gv2 = load.i64 notrap aligned gv1+4 +; nextln: stack_limit = gv2 +; nextln: +; nextln: block0(v0: i64 [%rdi], v5: i64 [%rbp]): +; nextln: v1 = load.i64 notrap aligned v0 +; nextln: v2 = load.i64 notrap aligned v1+4 +; nextln: v3 = iadd_imm v2, 32 +; nextln: v4 = ifcmp_sp v3 +; nextln: trapif uge v4, stk_ovf +; nextln: x86_push v5 +; nextln: copy_special %rsp -> %rbp +; nextln: adjust_sp_down_imm 32 +; nextln: adjust_sp_up_imm 32 +; nextln: v6 = x86_pop.i64 +; nextln: return v6 +; nextln: } diff --git a/cranelift/reader/src/parser.rs b/cranelift/reader/src/parser.rs index 172cb2d0993b..a64f6381065a 100644 --- a/cranelift/reader/src/parser.rs +++ b/cranelift/reader/src/parser.rs @@ -358,6 +358,15 @@ impl<'a> Context<'a> { Ok(()) } + // Configure the stack limit of the current function. + fn add_stack_limit(&mut self, limit: GlobalValue, loc: Location) -> ParseResult<()> { + if self.function.stack_limit.is_some() { + return err!(loc, "stack limit defined twice"); + } + self.function.stack_limit = Some(limit); + Ok(()) + } + // Resolve a reference to a constant. fn check_constant(&self, c: Constant, loc: Location) -> ParseResult<()> { if !self.map.contains_constant(c) { @@ -598,6 +607,15 @@ impl<'a> Parser<'a> { err!(self.loc, "expected constant number: const«n»") } + // Match and consume a stack limit token + fn match_stack_limit(&mut self) -> ParseResult<()> { + if let Some(Token::Identifier("stack_limit")) = self.token() { + self.consume(); + return Ok(()); + } + err!(self.loc, "expected identifier: stack_limit") + } + // Match and consume a block reference. fn match_block(&mut self, err_msg: &str) -> ParseResult { if let Some(Token::Block(block)) = self.token() { @@ -1455,6 +1473,7 @@ impl<'a> Parser<'a> { // * function-decl // * signature-decl // * jump-table-decl + // * stack-limit-decl // // The parsed decls are added to `ctx` rather than returned. fn parse_preamble(&mut self, ctx: &mut Context) -> ParseResult<()> { @@ -1503,6 +1522,11 @@ impl<'a> Parser<'a> { self.parse_constant_decl() .and_then(|(c, v)| ctx.add_constant(c, v, self.loc)) } + Some(Token::Identifier("stack_limit")) => { + self.start_gathering_comments(); + self.parse_stack_limit_decl() + .and_then(|gv| ctx.add_stack_limit(gv, self.loc)) + } // More to come.. _ => return Ok(()), }?; @@ -1907,6 +1931,28 @@ impl<'a> Parser<'a> { Ok((name, data)) } + // Parse a stack limit decl + // + // stack-limit-decl ::= * StackLimit "=" GlobalValue(gv) + fn parse_stack_limit_decl(&mut self) -> ParseResult { + self.match_stack_limit()?; + self.match_token(Token::Equal, "expected '=' in stack limit decl")?; + let limit = match self.token() { + Some(Token::GlobalValue(base_num)) => match GlobalValue::with_number(base_num) { + Some(gv) => gv, + None => return err!(self.loc, "invalid global value number for stack limit"), + }, + _ => return err!(self.loc, "expected global value"), + }; + self.consume(); + + // Collect any trailing comments. + self.token(); + self.claim_gathered_comments(AnyEntity::StackLimit); + + Ok(limit) + } + // Parse a function body, add contents to `ctx`. // // function-body ::= * { extended-basic-block } From 646b5676a06a8eea32754a92a3941de163858b04 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 20 Apr 2020 14:43:45 -0700 Subject: [PATCH 11/13] Fix new example for new apis --- examples/interrupt.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/interrupt.rs b/examples/interrupt.rs index 7f724668ffb0..696d8a2d952d 100644 --- a/examples/interrupt.rs +++ b/examples/interrupt.rs @@ -17,8 +17,7 @@ fn main() -> Result<()> { let module = Module::from_file(&store, "examples/interrupt.wat")?; let instance = Instance::new(&module, &[])?; let run = instance - .get_export("run") - .and_then(|e| e.func()) + .get_func("run") .ok_or(anyhow::format_err!("failed to find `run` function export"))? .get0::<()>()?; From 35c533fbd1a6cc6ca70df3405d301b3a9bee2e48 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 21 Apr 2020 07:20:10 -0700 Subject: [PATCH 12/13] Fix host segfault test in release mode --- crates/api/tests/iloop.rs | 28 ++++------------------------ crates/api/tests/stack-overflow.rs | 7 +------ tests/host_segfault.rs | 8 ++++++-- 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/crates/api/tests/iloop.rs b/crates/api/tests/iloop.rs index d496b3c79d3d..9e0f5aa851d2 100644 --- a/crates/api/tests/iloop.rs +++ b/crates/api/tests/iloop.rs @@ -27,12 +27,7 @@ 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_export("loop") - .unwrap() - .func() - .unwrap() - .get0::<()>()?; + let iloop = instance.get_func("loop").unwrap().get0::<()>()?; store.interrupt_handle()?.interrupt(); let trap = iloop().unwrap_err(); assert!(trap.message().contains("wasm trap: interrupt")); @@ -45,12 +40,7 @@ fn functions_interruptable() -> anyhow::Result<()> { let module = hugely_recursive_module(&store)?; let func = Func::wrap(&store, || {}); let instance = Instance::new(&module, &[func.into()])?; - let iloop = instance - .get_export("loop") - .unwrap() - .func() - .unwrap() - .get0::<()>()?; + let iloop = instance.get_func("loop").unwrap().get0::<()>()?; store.interrupt_handle()?.interrupt(); let trap = iloop().unwrap_err(); assert!( @@ -97,12 +87,7 @@ fn loop_interrupt_from_afar() -> anyhow::Result<()> { // Enter the infinitely looping function and assert that our interrupt // handle does indeed actually interrupt the function. - let iloop = instance - .get_export("loop") - .unwrap() - .func() - .unwrap() - .get0::<()>()?; + let iloop = instance.get_func("loop").unwrap().get0::<()>()?; let trap = iloop().unwrap_err(); thread.join().unwrap(); assert!( @@ -138,12 +123,7 @@ fn function_interrupt_from_afar() -> anyhow::Result<()> { // Enter the infinitely looping function and assert that our interrupt // handle does indeed actually interrupt the function. - let iloop = instance - .get_export("loop") - .unwrap() - .func() - .unwrap() - .get0::<()>()?; + let iloop = instance.get_func("loop").unwrap().get0::<()>()?; let trap = iloop().unwrap_err(); thread.join().unwrap(); assert!( diff --git a/crates/api/tests/stack-overflow.rs b/crates/api/tests/stack-overflow.rs index 9ec24b648f95..283426130ec5 100644 --- a/crates/api/tests/stack-overflow.rs +++ b/crates/api/tests/stack-overflow.rs @@ -24,12 +24,7 @@ fn host_always_has_some_stack() -> anyhow::Result<()> { )?; let func = Func::wrap(&store, test_host_stack); let instance = Instance::new(&module, &[func.into()])?; - let foo = instance - .get_export("foo") - .unwrap() - .func() - .unwrap() - .get0::<()>()?; + 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. diff --git a/tests/host_segfault.rs b/tests/host_segfault.rs index 4728008c42d6..a30e7288cd2c 100644 --- a/tests/host_segfault.rs +++ b/tests/host_segfault.rs @@ -46,7 +46,7 @@ fn main() { let store = Store::default(); let module = Module::new(&store, "(module)").unwrap(); let _instance = Instance::new(&module, &[]).unwrap(); - overrun_the_stack(); + println!("stack overrun: {}", overrun_the_stack()); }), ]; match env::var(VAR_NAME) { @@ -92,7 +92,11 @@ fn runtest(name: &str) { desc ); } else if name.contains("overrun the stack") { - assert!(stderr.contains("thread 'main' has overflowed its stack")); + assert!( + stderr.contains("thread 'main' has overflowed its stack"), + "bad stderr: {}", + stderr + ); } else { panic!("\n\nexpected a segfault on `{}`\n{}\n\n", name, desc); } From 9798fff37ada38826f351d5502e4a034872b3544 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 21 Apr 2020 09:58:07 -0700 Subject: [PATCH 13/13] Fix new doc example --- crates/api/src/runtime.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/api/src/runtime.rs b/crates/api/src/runtime.rs index be8fba056e3e..b2c154a42bcb 100644 --- a/crates/api/src/runtime.rs +++ b/crates/api/src/runtime.rs @@ -656,8 +656,7 @@ impl Store { /// "#)?; /// let instance = Instance::new(&module, &[])?; /// let run = instance - /// .get_export("run") - /// .and_then(|e| e.func()) + /// .get_func("run") /// .ok_or(anyhow::format_err!("failed to find `run` function export"))? /// .get0::<()>()?; ///