* 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
469 lines
14 KiB
Rust
469 lines
14 KiB
Rust
use crate::module::{EntityIndex, MemoryPlan, Module, TableElements, TablePlan};
|
|
use crate::tunables::Tunables;
|
|
use cranelift_codegen::ir;
|
|
use cranelift_codegen::ir::{AbiParam, ArgumentPurpose};
|
|
use cranelift_codegen::isa::TargetFrontendConfig;
|
|
use cranelift_entity::PrimaryMap;
|
|
use cranelift_wasm::{
|
|
self, translate_module, DataIndex, DefinedFuncIndex, ElemIndex, FuncIndex, Global, GlobalIndex,
|
|
Memory, MemoryIndex, ModuleTranslationState, SignatureIndex, Table, TableIndex,
|
|
TargetEnvironment, WasmError, WasmResult,
|
|
};
|
|
use std::convert::TryFrom;
|
|
use std::sync::Arc;
|
|
|
|
/// Contains function data: byte code and its offset in the module.
|
|
#[derive(Hash)]
|
|
pub struct FunctionBodyData<'a> {
|
|
/// Body byte code.
|
|
pub data: &'a [u8],
|
|
|
|
/// Body offset in the module file.
|
|
pub module_offset: usize,
|
|
}
|
|
|
|
/// The result of translating via `ModuleEnvironment`. Function bodies are not
|
|
/// yet translated, and data initializers have not yet been copied out of the
|
|
/// original buffer.
|
|
pub struct ModuleTranslation<'data> {
|
|
/// Compilation setting flags.
|
|
pub target_config: TargetFrontendConfig,
|
|
|
|
/// Module information.
|
|
pub module: Module,
|
|
|
|
/// References to the function bodies.
|
|
pub function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
|
|
|
/// References to the data initializers.
|
|
pub data_initializers: Vec<DataInitializer<'data>>,
|
|
|
|
/// Tunable parameters.
|
|
pub tunables: Tunables,
|
|
|
|
/// The decoded Wasm types for the module.
|
|
pub module_translation: Option<ModuleTranslationState>,
|
|
}
|
|
|
|
/// Object containing the standalone environment information.
|
|
pub struct ModuleEnvironment<'data> {
|
|
/// The result to be filled in.
|
|
result: ModuleTranslation<'data>,
|
|
}
|
|
|
|
impl<'data> ModuleEnvironment<'data> {
|
|
/// Allocates the environment data structures.
|
|
pub fn new(target_config: TargetFrontendConfig, tunables: &Tunables) -> Self {
|
|
Self {
|
|
result: ModuleTranslation {
|
|
target_config,
|
|
module: Module::new(),
|
|
function_body_inputs: PrimaryMap::new(),
|
|
data_initializers: Vec::new(),
|
|
tunables: tunables.clone(),
|
|
module_translation: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn pointer_type(&self) -> ir::Type {
|
|
self.result.target_config.pointer_type()
|
|
}
|
|
|
|
/// Translate a wasm module using this environment. This consumes the
|
|
/// `ModuleEnvironment` and produces a `ModuleTranslation`.
|
|
pub fn translate(mut self, data: &'data [u8]) -> WasmResult<ModuleTranslation<'data>> {
|
|
assert!(self.result.module_translation.is_none());
|
|
let module_translation = translate_module(data, &mut self)?;
|
|
self.result.module_translation = Some(module_translation);
|
|
Ok(self.result)
|
|
}
|
|
|
|
fn declare_export(&mut self, export: EntityIndex, name: &str) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.exports
|
|
.insert(String::from(name), export);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'data> TargetEnvironment for ModuleEnvironment<'data> {
|
|
fn target_config(&self) -> TargetFrontendConfig {
|
|
self.result.target_config
|
|
}
|
|
}
|
|
|
|
/// This trait is useful for `translate_module` because it tells how to translate
|
|
/// environment-dependent wasm instructions. These functions should not be called by the user.
|
|
impl<'data> cranelift_wasm::ModuleEnvironment<'data> for ModuleEnvironment<'data> {
|
|
fn reserve_signatures(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.local
|
|
.signatures
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_signature(&mut self, sig: ir::Signature) -> WasmResult<()> {
|
|
let sig = translate_signature(sig, self.pointer_type());
|
|
// TODO: Deduplicate signatures.
|
|
self.result.module.local.signatures.push(sig);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_imports(&mut self, num: u32) -> WasmResult<()> {
|
|
Ok(self
|
|
.result
|
|
.module
|
|
.imports
|
|
.reserve_exact(usize::try_from(num).unwrap()))
|
|
}
|
|
|
|
fn declare_func_import(
|
|
&mut self,
|
|
sig_index: SignatureIndex,
|
|
module: &str,
|
|
field: &str,
|
|
) -> WasmResult<()> {
|
|
debug_assert_eq!(
|
|
self.result.module.local.functions.len(),
|
|
self.result.module.local.num_imported_funcs,
|
|
"Imported functions must be declared first"
|
|
);
|
|
let func_index = self.result.module.local.functions.push(sig_index);
|
|
self.result.module.imports.push((
|
|
module.to_owned(),
|
|
field.to_owned(),
|
|
EntityIndex::Function(func_index),
|
|
));
|
|
self.result.module.local.num_imported_funcs += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_table_import(&mut self, table: Table, module: &str, field: &str) -> WasmResult<()> {
|
|
debug_assert_eq!(
|
|
self.result.module.local.table_plans.len(),
|
|
self.result.module.local.num_imported_tables,
|
|
"Imported tables must be declared first"
|
|
);
|
|
let plan = TablePlan::for_table(table, &self.result.tunables);
|
|
let table_index = self.result.module.local.table_plans.push(plan);
|
|
self.result.module.imports.push((
|
|
module.to_owned(),
|
|
field.to_owned(),
|
|
EntityIndex::Table(table_index),
|
|
));
|
|
self.result.module.local.num_imported_tables += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_memory_import(
|
|
&mut self,
|
|
memory: Memory,
|
|
module: &str,
|
|
field: &str,
|
|
) -> WasmResult<()> {
|
|
debug_assert_eq!(
|
|
self.result.module.local.memory_plans.len(),
|
|
self.result.module.local.num_imported_memories,
|
|
"Imported memories must be declared first"
|
|
);
|
|
if memory.shared {
|
|
return Err(WasmError::Unsupported("shared memories".to_owned()));
|
|
}
|
|
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
|
let memory_index = self.result.module.local.memory_plans.push(plan);
|
|
self.result.module.imports.push((
|
|
module.to_owned(),
|
|
field.to_owned(),
|
|
EntityIndex::Memory(memory_index),
|
|
));
|
|
self.result.module.local.num_imported_memories += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_global_import(
|
|
&mut self,
|
|
global: Global,
|
|
module: &str,
|
|
field: &str,
|
|
) -> WasmResult<()> {
|
|
debug_assert_eq!(
|
|
self.result.module.local.globals.len(),
|
|
self.result.module.local.num_imported_globals,
|
|
"Imported globals must be declared first"
|
|
);
|
|
let global_index = self.result.module.local.globals.push(global);
|
|
self.result.module.imports.push((
|
|
module.to_owned(),
|
|
field.to_owned(),
|
|
EntityIndex::Global(global_index),
|
|
));
|
|
self.result.module.local.num_imported_globals += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_func_types(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.local
|
|
.functions
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
self.result
|
|
.function_body_inputs
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_func_type(&mut self, sig_index: SignatureIndex) -> WasmResult<()> {
|
|
self.result.module.local.functions.push(sig_index);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_tables(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.local
|
|
.table_plans
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_table(&mut self, table: Table) -> WasmResult<()> {
|
|
let plan = TablePlan::for_table(table, &self.result.tunables);
|
|
self.result.module.local.table_plans.push(plan);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_memories(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.local
|
|
.memory_plans
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_memory(&mut self, memory: Memory) -> WasmResult<()> {
|
|
if memory.shared {
|
|
return Err(WasmError::Unsupported("shared memories".to_owned()));
|
|
}
|
|
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
|
self.result.module.local.memory_plans.push(plan);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_globals(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.local
|
|
.globals
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_global(&mut self, global: Global) -> WasmResult<()> {
|
|
self.result.module.local.globals.push(global);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_exports(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.exports
|
|
.reserve(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_func_export(&mut self, func_index: FuncIndex, name: &str) -> WasmResult<()> {
|
|
self.declare_export(EntityIndex::Function(func_index), name)
|
|
}
|
|
|
|
fn declare_table_export(&mut self, table_index: TableIndex, name: &str) -> WasmResult<()> {
|
|
self.declare_export(EntityIndex::Table(table_index), name)
|
|
}
|
|
|
|
fn declare_memory_export(&mut self, memory_index: MemoryIndex, name: &str) -> WasmResult<()> {
|
|
self.declare_export(EntityIndex::Memory(memory_index), name)
|
|
}
|
|
|
|
fn declare_global_export(&mut self, global_index: GlobalIndex, name: &str) -> WasmResult<()> {
|
|
self.declare_export(EntityIndex::Global(global_index), name)
|
|
}
|
|
|
|
fn declare_start_func(&mut self, func_index: FuncIndex) -> WasmResult<()> {
|
|
debug_assert!(self.result.module.start_func.is_none());
|
|
self.result.module.start_func = Some(func_index);
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_table_elements(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.table_elements
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_table_elements(
|
|
&mut self,
|
|
table_index: TableIndex,
|
|
base: Option<GlobalIndex>,
|
|
offset: usize,
|
|
elements: Box<[FuncIndex]>,
|
|
) -> WasmResult<()> {
|
|
self.result.module.table_elements.push(TableElements {
|
|
table_index,
|
|
base,
|
|
offset,
|
|
elements,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_passive_element(
|
|
&mut self,
|
|
elem_index: ElemIndex,
|
|
segments: Box<[FuncIndex]>,
|
|
) -> WasmResult<()> {
|
|
let old = self
|
|
.result
|
|
.module
|
|
.passive_elements
|
|
.insert(elem_index, segments);
|
|
debug_assert!(
|
|
old.is_none(),
|
|
"should never get duplicate element indices, that would be a bug in `cranelift_wasm`'s \
|
|
translation"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn define_function_body(
|
|
&mut self,
|
|
_module_translation: &ModuleTranslationState,
|
|
body_bytes: &'data [u8],
|
|
body_offset: usize,
|
|
) -> WasmResult<()> {
|
|
self.result.function_body_inputs.push(FunctionBodyData {
|
|
data: body_bytes,
|
|
module_offset: body_offset,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_data_initializers(&mut self, num: u32) -> WasmResult<()> {
|
|
self.result
|
|
.data_initializers
|
|
.reserve_exact(usize::try_from(num).unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_data_initialization(
|
|
&mut self,
|
|
memory_index: MemoryIndex,
|
|
base: Option<GlobalIndex>,
|
|
offset: usize,
|
|
data: &'data [u8],
|
|
) -> WasmResult<()> {
|
|
self.result.data_initializers.push(DataInitializer {
|
|
location: DataInitializerLocation {
|
|
memory_index,
|
|
base,
|
|
offset,
|
|
},
|
|
data,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn reserve_passive_data(&mut self, count: u32) -> WasmResult<()> {
|
|
self.result.module.passive_data.reserve(count as usize);
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_passive_data(&mut self, data_index: DataIndex, data: &'data [u8]) -> WasmResult<()> {
|
|
let old = self
|
|
.result
|
|
.module
|
|
.passive_data
|
|
.insert(data_index, Arc::from(data));
|
|
debug_assert!(
|
|
old.is_none(),
|
|
"a module can't have duplicate indices, this would be a cranelift-wasm bug"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_module_name(&mut self, name: &'data str) -> WasmResult<()> {
|
|
self.result.module.name = Some(name.to_string());
|
|
Ok(())
|
|
}
|
|
|
|
fn declare_func_name(&mut self, func_index: FuncIndex, name: &'data str) -> WasmResult<()> {
|
|
self.result
|
|
.module
|
|
.func_names
|
|
.insert(func_index, name.to_string());
|
|
Ok(())
|
|
}
|
|
|
|
fn custom_section(&mut self, name: &'data str, _data: &'data [u8]) -> WasmResult<()> {
|
|
match name {
|
|
"webidl-bindings" | "wasm-interface-types" => Err(WasmError::Unsupported(
|
|
"\
|
|
Support for interface types has temporarily been removed from `wasmtime`.
|
|
|
|
For more information about this temoprary you can read on the issue online:
|
|
|
|
https://github.com/bytecodealliance/wasmtime/issues/1271
|
|
|
|
and for re-adding support for interface types you can see this issue:
|
|
|
|
https://github.com/bytecodealliance/wasmtime/issues/677
|
|
"
|
|
.to_owned(),
|
|
)),
|
|
// skip other sections
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add environment-specific function parameters.
|
|
pub fn translate_signature(mut sig: ir::Signature, pointer_type: ir::Type) -> ir::Signature {
|
|
// Prepend the vmctx argument.
|
|
sig.params.insert(
|
|
0,
|
|
AbiParam::special(pointer_type, ArgumentPurpose::VMContext),
|
|
);
|
|
// Prepend the caller vmctx argument.
|
|
sig.params.insert(1, AbiParam::new(pointer_type));
|
|
sig
|
|
}
|
|
|
|
/// A memory index and offset within that memory where a data initialization
|
|
/// should is to be performed.
|
|
#[derive(Clone)]
|
|
pub struct DataInitializerLocation {
|
|
/// The index of the memory to initialize.
|
|
pub memory_index: MemoryIndex,
|
|
|
|
/// Optionally a globalvar base to initialize at.
|
|
pub base: Option<GlobalIndex>,
|
|
|
|
/// A constant offset to initialize at.
|
|
pub offset: usize,
|
|
}
|
|
|
|
/// A data initializer for linear memory.
|
|
pub struct DataInitializer<'data> {
|
|
/// The location where the initialization is to be performed.
|
|
pub location: DataInitializerLocation,
|
|
|
|
/// The initialization data.
|
|
pub data: &'data [u8],
|
|
}
|