* Implement interrupting wasm code, reimplement stack overflow This commit is a relatively large change for wasmtime with two main goals: * Primarily this enables interrupting executing wasm code with a trap, preventing infinite loops in wasm code. Note that resumption of the wasm code is not a goal of this commit. * Additionally this commit reimplements how we handle stack overflow to ensure that host functions always have a reasonable amount of stack to run on. This fixes an issue where we might longjmp out of a host function, skipping destructors. Lots of various odds and ends end up falling out in this commit once the two goals above were implemented. The strategy for implementing this was also lifted from Spidermonkey and existing functionality inside of Cranelift. I've tried to write up thorough documentation of how this all works in `crates/environ/src/cranelift.rs` where gnarly-ish bits are. A brief summary of how this works is that each function and each loop header now checks to see if they're interrupted. Interrupts and the stack overflow check are actually folded into one now, where function headers check to see if they've run out of stack and the sentinel value used to indicate an interrupt, checked in loop headers, tricks functions into thinking they're out of stack. An interrupt is basically just writing a value to a location which is read by JIT code. When interrupts are delivered and what triggers them has been left up to embedders of the `wasmtime` crate. The `wasmtime::Store` type has a method to acquire an `InterruptHandle`, where `InterruptHandle` is a `Send` and `Sync` type which can travel to other threads (or perhaps even a signal handler) to get notified from. It's intended that this provides a good degree of flexibility when interrupting wasm code. Note though that this does have a large caveat where interrupts don't work when you're interrupting host code, so if you've got a host import blocking for a long time an interrupt won't actually be received until the wasm starts running again. Some fallout included from this change is: * Unix signal handlers are no longer registered with `SA_ONSTACK`. Instead they run on the native stack the thread was already using. This is possible since stack overflow isn't handled by hitting the guard page, but rather it's explicitly checked for in wasm now. Native stack overflow will continue to abort the process as usual. * Unix sigaltstack management is now no longer necessary since we don't use it any more. * Windows no longer has any need to reset guard pages since we no longer try to recover from faults on guard pages. * On all targets probestack intrinsics are disabled since we use a different mechanism for catching stack overflow. * The C API has been updated with interrupts handles. An example has also been added which shows off how to interrupt a module. Closes #139 Closes #860 Closes #900 * Update comment about magical interrupt value * Store stack limit as a global value, not a closure * Run rustfmt * Handle review comments * Add a comment about SA_ONSTACK * Use `usize` for type of `INTERRUPTED` * Parse human-readable durations * Bring back sigaltstack handling Allows libstd to print out stack overflow on failure still. * Add parsing and emission of stack limit-via-preamble * Fix new example for new apis * Fix host segfault test in release mode * Fix new doc example
246 lines
8.6 KiB
Rust
246 lines
8.6 KiB
Rust
//! Generating sequences of Wasmtime API calls.
|
|
//!
|
|
//! We only generate *valid* sequences of API calls. To do this, we keep track
|
|
//! of what objects we've already created in earlier API calls via the `Scope`
|
|
//! struct.
|
|
//!
|
|
//! To generate even-more-pathological sequences of API calls, we use [swarm
|
|
//! testing]:
|
|
//!
|
|
//! > In swarm testing, the usual practice of potentially including all features
|
|
//! > in every test case is abandoned. Rather, a large “swarm” of randomly
|
|
//! > generated configurations, each of which omits some features, is used, with
|
|
//! > configurations receiving equal resources.
|
|
//!
|
|
//! [swarm testing]: https://www.cs.utah.edu/~regehr/papers/swarm12.pdf
|
|
|
|
use arbitrary::{Arbitrary, Unstructured};
|
|
use std::collections::BTreeMap;
|
|
use std::mem;
|
|
use wasmparser::*;
|
|
|
|
#[derive(Arbitrary, Debug)]
|
|
struct Swarm {
|
|
config_debug_info: bool,
|
|
config_interruptable: bool,
|
|
module_new: bool,
|
|
module_drop: bool,
|
|
instance_new: bool,
|
|
instance_drop: bool,
|
|
call_exported_func: bool,
|
|
}
|
|
|
|
/// A call to one of Wasmtime's public APIs.
|
|
#[derive(Arbitrary, Clone, Debug)]
|
|
#[allow(missing_docs)]
|
|
pub enum ApiCall {
|
|
ConfigNew,
|
|
ConfigDebugInfo(bool),
|
|
ConfigInterruptable(bool),
|
|
EngineNew,
|
|
StoreNew,
|
|
ModuleNew { id: usize, wasm: super::WasmOptTtf },
|
|
ModuleDrop { id: usize },
|
|
InstanceNew { id: usize, module: usize },
|
|
InstanceDrop { id: usize },
|
|
CallExportedFunc { instance: usize, nth: usize },
|
|
}
|
|
use ApiCall::*;
|
|
|
|
#[derive(Default)]
|
|
struct Scope {
|
|
id_counter: usize,
|
|
|
|
/// Map from a module id to the predicted amount of rss it will take to
|
|
/// instantiate.
|
|
modules: BTreeMap<usize, usize>,
|
|
|
|
/// Map from an instance id to the amount of rss it's expected to be using.
|
|
instances: BTreeMap<usize, usize>,
|
|
|
|
/// The rough predicted maximum RSS of executing all of our generated API
|
|
/// calls thus far.
|
|
predicted_rss: usize,
|
|
}
|
|
|
|
impl Scope {
|
|
fn next_id(&mut self) -> usize {
|
|
let id = self.id_counter;
|
|
self.id_counter = id + 1;
|
|
id
|
|
}
|
|
}
|
|
|
|
/// A sequence of API calls.
|
|
#[derive(Debug)]
|
|
pub struct ApiCalls {
|
|
/// The API calls.
|
|
pub calls: Vec<ApiCall>,
|
|
}
|
|
|
|
impl Arbitrary for ApiCalls {
|
|
fn arbitrary(input: &mut Unstructured) -> arbitrary::Result<Self> {
|
|
crate::init_fuzzing();
|
|
|
|
let swarm = Swarm::arbitrary(input)?;
|
|
let mut calls = vec![];
|
|
|
|
arbitrary_config(input, &swarm, &mut calls)?;
|
|
calls.push(EngineNew);
|
|
calls.push(StoreNew);
|
|
|
|
let mut scope = Scope::default();
|
|
let max_rss = 1 << 30; // 1GB
|
|
|
|
// Total limit on number of API calls we'll generate. This exists to
|
|
// avoid libFuzzer timeouts.
|
|
let max_calls = 100;
|
|
|
|
for _ in 0..input.arbitrary_len::<ApiCall>()? {
|
|
if calls.len() > max_calls {
|
|
break;
|
|
}
|
|
|
|
let mut choices: Vec<fn(_, &mut Scope) -> arbitrary::Result<ApiCall>> = vec![];
|
|
|
|
if swarm.module_new {
|
|
choices.push(|input, scope| {
|
|
let id = scope.next_id();
|
|
let wasm = super::WasmOptTtf::arbitrary(input)?;
|
|
let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0);
|
|
scope.modules.insert(id, predicted_rss);
|
|
Ok(ModuleNew { id, wasm })
|
|
});
|
|
}
|
|
if swarm.module_drop && !scope.modules.is_empty() {
|
|
choices.push(|input, scope| {
|
|
let modules: Vec<_> = scope.modules.keys().collect();
|
|
let id = **input.choose(&modules)?;
|
|
scope.modules.remove(&id);
|
|
Ok(ModuleDrop { id })
|
|
});
|
|
}
|
|
if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss {
|
|
choices.push(|input, scope| {
|
|
let modules: Vec<_> = scope.modules.iter().collect();
|
|
let (&module, &predicted_rss) = *input.choose(&modules)?;
|
|
let id = scope.next_id();
|
|
scope.instances.insert(id, predicted_rss);
|
|
scope.predicted_rss += predicted_rss;
|
|
Ok(InstanceNew { id, module })
|
|
});
|
|
}
|
|
if swarm.instance_drop && !scope.instances.is_empty() {
|
|
choices.push(|input, scope| {
|
|
let instances: Vec<_> = scope.instances.iter().collect();
|
|
let (&id, &rss) = *input.choose(&instances)?;
|
|
scope.instances.remove(&id);
|
|
scope.predicted_rss -= rss;
|
|
Ok(InstanceDrop { id })
|
|
});
|
|
}
|
|
if swarm.call_exported_func && !scope.instances.is_empty() {
|
|
choices.push(|input, scope| {
|
|
let instances: Vec<_> = scope.instances.keys().collect();
|
|
let instance = **input.choose(&instances)?;
|
|
let nth = usize::arbitrary(input)?;
|
|
Ok(CallExportedFunc { instance, nth })
|
|
});
|
|
}
|
|
|
|
if choices.is_empty() {
|
|
break;
|
|
}
|
|
let c = input.choose(&choices)?;
|
|
calls.push(c(input, &mut scope)?);
|
|
}
|
|
|
|
Ok(ApiCalls { calls })
|
|
}
|
|
|
|
fn size_hint(depth: usize) -> (usize, Option<usize>) {
|
|
arbitrary::size_hint::recursion_guard(depth, |depth| {
|
|
arbitrary::size_hint::or(
|
|
// This is the stuff we unconditionally need, which affects the
|
|
// minimum size.
|
|
arbitrary::size_hint::and(
|
|
<Swarm 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
|
|
// the upper bound to `None`.
|
|
<super::WasmOptTtf as Arbitrary>::size_hint(depth),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
fn arbitrary_config(
|
|
input: &mut Unstructured,
|
|
swarm: &Swarm,
|
|
calls: &mut Vec<ApiCall>,
|
|
) -> arbitrary::Result<()> {
|
|
calls.push(ConfigNew);
|
|
|
|
if swarm.config_debug_info && bool::arbitrary(input)? {
|
|
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(())
|
|
}
|
|
|
|
/// Attempt to heuristically predict how much rss instantiating the `wasm`
|
|
/// provided will take in wasmtime.
|
|
///
|
|
/// The intention of this function is to prevent out-of-memory situations from
|
|
/// trivially instantiating a bunch of modules. We're basically taking any
|
|
/// random sequence of fuzz inputs and generating API calls, but if we
|
|
/// instantiate a million things we'd reasonably expect that to exceed the fuzz
|
|
/// limit of 2GB because, well, instantiation does take a bit of memory.
|
|
///
|
|
/// This prediction will prevent new instances from being created once we've
|
|
/// created a bunch of instances. Once instances start being dropped, though,
|
|
/// it'll free up new slots to start making new instances.
|
|
fn predict_rss(wasm: &[u8]) -> Result<usize> {
|
|
let mut prediction = 0;
|
|
let mut reader = ModuleReader::new(wasm)?;
|
|
while !reader.eof() {
|
|
let section = reader.read()?;
|
|
match section.code {
|
|
// For each declared memory we'll have to map that all in, so add in
|
|
// the minimum amount of memory to our predicted rss.
|
|
SectionCode::Memory => {
|
|
for entry in section.get_memory_section_reader()? {
|
|
let initial = entry?.limits.initial as usize;
|
|
prediction += initial * 64 * 1024;
|
|
}
|
|
}
|
|
|
|
// We'll need to allocate tables and space for table elements, and
|
|
// currently this is 3 pointers per table entry.
|
|
SectionCode::Table => {
|
|
for entry in section.get_table_section_reader()? {
|
|
let initial = entry?.limits.initial as usize;
|
|
prediction += initial * 3 * mem::size_of::<usize>();
|
|
}
|
|
}
|
|
|
|
// ... and for now nothing else is counted. If we run into issues
|
|
// with the fuzzers though we can always try to take into account
|
|
// more things
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(prediction)
|
|
}
|