handle traps naively

This commit is contained in:
Lachlan Sneff 2019-01-18 16:45:30 -08:00
parent c18328aa4c
commit ebeea0c71c
8 changed files with 199 additions and 67 deletions

View File

@ -1,6 +1,8 @@
mod recovery; mod recovery;
mod sighandler; mod sighandler;
pub use self::recovery::HandlerData;
use crate::call::recovery::call_protected; use crate::call::recovery::call_protected;
use hashbrown::HashSet; use hashbrown::HashSet;
use libffi::high::{arg as libffi_arg, call as libffi_call, CodePtr}; use libffi::high::{arg as libffi_arg, call as libffi_call, CodePtr};
@ -16,10 +18,11 @@ use wasmer_runtime::{
pub struct Caller { pub struct Caller {
func_export_set: HashSet<FuncIndex>, func_export_set: HashSet<FuncIndex>,
handler_data: HandlerData,
} }
impl Caller { impl Caller {
pub fn new(module: &ModuleInner) -> Self { pub fn new(module: &ModuleInner, handler_data: HandlerData) -> Self {
let mut func_export_set = HashSet::new(); let mut func_export_set = HashSet::new();
for export_index in module.exports.values() { for export_index in module.exports.values() {
if let ExportIndex::Func(func_index) = export_index { if let ExportIndex::Func(func_index) = export_index {
@ -30,7 +33,10 @@ impl Caller {
func_export_set.insert(start_func_index); func_export_set.insert(start_func_index);
} }
Self { func_export_set } Self {
func_export_set,
handler_data,
}
} }
} }
@ -74,7 +80,7 @@ impl ProtectedCaller for Caller {
let code_ptr = CodePtr::from_ptr(func_ptr as _); let code_ptr = CodePtr::from_ptr(func_ptr as _);
call_protected(|| { call_protected(&self.handler_data, || {
// Only supports zero or one return values for now. // Only supports zero or one return values for now.
// To support multiple returns, we will have to // To support multiple returns, we will have to
// generate trampolines instead of using libffi. // generate trampolines instead of using libffi.

View File

@ -5,11 +5,18 @@
//! unless you have memory unsafety elsewhere in your code. //! unless you have memory unsafety elsewhere in your code.
use crate::call::sighandler::install_sighandler; use crate::call::sighandler::install_sighandler;
use nix::libc::siginfo_t; use crate::relocation::{TrapData, TrapSink};
use cranelift_codegen::ir::TrapCode;
use nix::libc::{c_void, siginfo_t};
use nix::sys::signal::{Signal, SIGBUS, SIGFPE, SIGILL, SIGSEGV}; use nix::sys::signal::{Signal, SIGBUS, SIGFPE, SIGILL, SIGSEGV};
use std::cell::{Cell, UnsafeCell}; use std::cell::{Cell, UnsafeCell};
use std::ptr;
use std::sync::Once; use std::sync::Once;
use wasmer_runtime::error::{RuntimeError, RuntimeResult}; use wasmer_runtime::{
error::{RuntimeError, RuntimeResult},
structures::TypedIndex,
types::{MemoryIndex, TableIndex},
};
extern "C" { extern "C" {
pub fn setjmp(env: *mut ::nix::libc::c_void) -> ::nix::libc::c_int; pub fn setjmp(env: *mut ::nix::libc::c_void) -> ::nix::libc::c_int;
@ -21,10 +28,39 @@ pub static SIGHANDLER_INIT: Once = Once::new();
thread_local! { thread_local! {
pub static SETJMP_BUFFER: UnsafeCell<[::nix::libc::c_int; SETJMP_BUFFER_LEN]> = UnsafeCell::new([0; SETJMP_BUFFER_LEN]); pub static SETJMP_BUFFER: UnsafeCell<[::nix::libc::c_int; SETJMP_BUFFER_LEN]> = UnsafeCell::new([0; SETJMP_BUFFER_LEN]);
pub static CAUGHT_ADDRESS: Cell<usize> = Cell::new(0); pub static CAUGHT_ADDRESSES: Cell<(*const c_void, *const c_void)> = Cell::new((ptr::null(), ptr::null()));
pub static CURRENT_EXECUTABLE_BUFFER: Cell<*const c_void> = Cell::new(ptr::null());
} }
pub fn call_protected<T>(f: impl FnOnce() -> T) -> RuntimeResult<T> { pub struct HandlerData {
trap_data: TrapSink,
buffer_ptr: *const c_void,
buffer_size: usize,
}
impl HandlerData {
pub fn new(trap_data: TrapSink, buffer_ptr: *const c_void, buffer_size: usize) -> Self {
Self {
trap_data,
buffer_ptr,
buffer_size,
}
}
pub fn lookup(&self, ip: *const c_void) -> Option<TrapData> {
let ip = ip as usize;
let buffer_ptr = self.buffer_ptr as usize;
if buffer_ptr <= ip && ip < buffer_ptr + self.buffer_size {
let offset = ip - buffer_ptr;
self.trap_data.lookup(offset)
} else {
None
}
}
}
pub fn call_protected<T>(handler_data: &HandlerData, f: impl FnOnce() -> T) -> RuntimeResult<T> {
unsafe { unsafe {
let jmp_buf = SETJMP_BUFFER.with(|buf| buf.get()); let jmp_buf = SETJMP_BUFFER.with(|buf| buf.get());
let prev_jmp_buf = *jmp_buf; let prev_jmp_buf = *jmp_buf;
@ -36,8 +72,45 @@ pub fn call_protected<T>(f: impl FnOnce() -> T) -> RuntimeResult<T> {
let signum = setjmp(jmp_buf as *mut ::nix::libc::c_void); let signum = setjmp(jmp_buf as *mut ::nix::libc::c_void);
if signum != 0 { if signum != 0 {
*jmp_buf = prev_jmp_buf; *jmp_buf = prev_jmp_buf;
let addr = CAUGHT_ADDRESS.with(|cell| cell.get()); let (faulting_addr, _) = CAUGHT_ADDRESSES.with(|cell| cell.get());
if let Some(TrapData {
trapcode,
srcloc: _,
}) = handler_data.lookup(faulting_addr)
{
Err(match Signal::from_c_int(signum) {
Ok(SIGILL) => match trapcode {
TrapCode::BadSignature => RuntimeError::IndirectCallSignature {
table: TableIndex::new(0),
},
TrapCode::IndirectCallToNull => RuntimeError::IndirectCallToNull {
table: TableIndex::new(0),
},
TrapCode::HeapOutOfBounds => RuntimeError::OutOfBoundsAccess {
memory: MemoryIndex::new(0),
addr: 0,
},
TrapCode::TableOutOfBounds => RuntimeError::TableOutOfBounds {
table: TableIndex::new(0),
},
_ => RuntimeError::Unknown {
msg: "unknown trap".to_string(),
},
},
Ok(SIGSEGV) | Ok(SIGBUS) => {
// I'm too lazy right now to actually check if the address is within one of the memories,
// so just say that it's a memory-out-of-bounds access for now
RuntimeError::OutOfBoundsAccess {
memory: MemoryIndex::new(0),
addr: 0,
}
}
Ok(SIGFPE) => RuntimeError::IllegalArithmeticOperation,
_ => unimplemented!(),
}
.into())
} else {
let signal = match Signal::from_c_int(signum) { let signal = match Signal::from_c_int(signum) {
Ok(SIGFPE) => "floating-point exception", Ok(SIGFPE) => "floating-point exception",
Ok(SIGILL) => "illegal instruction", Ok(SIGILL) => "illegal instruction",
@ -48,9 +121,10 @@ pub fn call_protected<T>(f: impl FnOnce() -> T) -> RuntimeResult<T> {
}; };
// When the trap-handler is fully implemented, this will return more information. // When the trap-handler is fully implemented, this will return more information.
Err(RuntimeError::Unknown { Err(RuntimeError::Unknown {
msg: format!("trap at {:#x} - {}", addr, signal), msg: format!("trap at {:p} - {}", faulting_addr, signal),
} }
.into()) .into())
}
} else { } else {
let ret = f(); // TODO: Switch stack? let ret = f(); // TODO: Switch stack?
*jmp_buf = prev_jmp_buf; *jmp_buf = prev_jmp_buf;
@ -60,7 +134,7 @@ pub fn call_protected<T>(f: impl FnOnce() -> T) -> RuntimeResult<T> {
} }
/// Unwinds to last protected_call. /// Unwinds to last protected_call.
pub unsafe fn do_unwind(signum: i32, siginfo: *mut siginfo_t) -> ! { pub unsafe fn do_unwind(signum: i32, siginfo: *mut siginfo_t, ucontext: *const c_void) -> ! {
// Since do_unwind is only expected to get called from WebAssembly code which doesn't hold any host resources (locks etc.) // Since do_unwind is only expected to get called from WebAssembly code which doesn't hold any host resources (locks etc.)
// itself, accessing TLS here is safe. In case any other code calls this, it often indicates a memory safety bug and you should // itself, accessing TLS here is safe. In case any other code calls this, it often indicates a memory safety bug and you should
// temporarily disable the signal handlers to debug it. // temporarily disable the signal handlers to debug it.
@ -69,9 +143,30 @@ pub unsafe fn do_unwind(signum: i32, siginfo: *mut siginfo_t) -> ! {
if *jmp_buf == [0; SETJMP_BUFFER_LEN] { if *jmp_buf == [0; SETJMP_BUFFER_LEN] {
::std::process::abort(); ::std::process::abort();
} }
// We only target macos at the moment as other ones might not have si_addr field
#[cfg(target_os = "macos")] CAUGHT_ADDRESSES.with(|cell| cell.set(get_faulting_addr_and_ip(siginfo, ucontext)));
CAUGHT_ADDRESS.with(|cell| cell.set((*siginfo).si_addr as _));
longjmp(jmp_buf as *mut ::nix::libc::c_void, signum) longjmp(jmp_buf as *mut ::nix::libc::c_void, signum)
} }
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
unsafe fn get_faulting_addr_and_ip(
siginfo: *mut siginfo_t,
_ucontext: *const c_void,
) -> (*const c_void, *const c_void) {
(ptr::null(), ptr::null())
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
unsafe fn get_faulting_addr_and_ip(
siginfo: *mut siginfo_t,
_ucontext: *const c_void,
) -> (*const c_void, *const c_void) {
((*siginfo).si_addr, ptr::null())
}
#[cfg(not(any(
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64"),
)))]
compile_error!("This crate doesn't yet support compiling on operating systems other than linux and macos and architectures other than x86_64");

View File

@ -1,8 +1,6 @@
//! We install signal handlers to handle WebAssembly traps within //! Installing signal handlers allows us to handle traps and out-of-bounds memory
//! our Rust code. Otherwise we will have errors that stop the Rust process //! accesses that occur when runniing webassembly.
//! such as `process didn't exit successfully: ... (signal: 8, SIGFPE: erroneous arithmetic operation)`
//! //!
//! Please read more about this here: https://github.com/CraneStation/wasmtime/issues/15
//! This code is inspired by: https://github.com/pepyakin/wasmtime/commit/625a2b6c0815b21996e111da51b9664feb174622 //! This code is inspired by: https://github.com/pepyakin/wasmtime/commit/625a2b6c0815b21996e111da51b9664feb174622
use crate::call::recovery; use crate::call::recovery;
use nix::libc::{c_void, siginfo_t}; use nix::libc::{c_void, siginfo_t};
@ -25,9 +23,9 @@ pub unsafe fn install_sighandler() {
extern "C" fn signal_trap_handler( extern "C" fn signal_trap_handler(
signum: ::nix::libc::c_int, signum: ::nix::libc::c_int,
siginfo: *mut siginfo_t, siginfo: *mut siginfo_t,
_ucontext: *mut c_void, ucontext: *mut c_void,
) { ) {
unsafe { unsafe {
recovery::do_unwind(signum, siginfo); recovery::do_unwind(signum, siginfo, ucontext);
} }
} }

View File

@ -94,10 +94,10 @@ impl Module {
*sig_index = sig_registry.lookup_deduplicated_sigindex(*sig_index); *sig_index = sig_registry.lookup_deduplicated_sigindex(*sig_index);
}); });
let func_resolver_builder = FuncResolverBuilder::new(isa, functions)?; let (func_resolver_builder, handler_data) = FuncResolverBuilder::new(isa, functions)?;
self.module.func_resolver = Box::new(func_resolver_builder.finalize()?); self.module.func_resolver = Box::new(func_resolver_builder.finalize()?);
self.module.protected_caller = Box::new(Caller::new(&self.module)); self.module.protected_caller = Box::new(Caller::new(&self.module, handler_data));
Ok(self.module) Ok(self.module)
} }

View File

@ -3,10 +3,10 @@
//! any other calls that this function is doing, so we can "patch" the //! any other calls that this function is doing, so we can "patch" the
//! function addrs in runtime with the functions we need. //! function addrs in runtime with the functions we need.
use cranelift_codegen::binemit; use cranelift_codegen::binemit;
use cranelift_codegen::ir::{self, ExternalName, LibCall, SourceLoc, TrapCode};
use wasmer_runtime::{structures::TypedIndex, types::LocalFuncIndex};
pub use cranelift_codegen::binemit::Reloc; pub use cranelift_codegen::binemit::Reloc;
use cranelift_codegen::ir::{self, ExternalName, LibCall, SourceLoc, TrapCode};
use hashbrown::HashMap;
use wasmer_runtime::{structures::TypedIndex, types::LocalFuncIndex};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Relocation { pub struct Relocation {
@ -133,30 +133,50 @@ impl RelocSink {
} }
} }
#[derive(Debug, Clone, Copy)]
pub struct TrapData { pub struct TrapData {
pub offset: usize, pub trapcode: TrapCode,
pub code: TrapCode, pub srcloc: SourceLoc,
} }
/// Simple implementation of a TrapSink /// Simple implementation of a TrapSink
/// that saves the info for later. /// that saves the info for later.
pub struct TrapSink { pub struct TrapSink {
trap_datas: Vec<TrapData>, trap_datas: HashMap<usize, TrapData>,
} }
impl TrapSink { impl TrapSink {
pub fn new() -> TrapSink { pub fn new() -> TrapSink {
TrapSink { TrapSink {
trap_datas: Vec::new(), trap_datas: HashMap::new(),
}
} }
} }
impl binemit::TrapSink for TrapSink { pub fn lookup(&self, offset: usize) -> Option<TrapData> {
fn trap(&mut self, offset: u32, _: SourceLoc, code: TrapCode) { self.trap_datas.get(&offset).cloned()
self.trap_datas.push(TrapData { }
offset: offset as usize,
code, pub fn drain_local(&mut self, current_func_offset: usize, local: &mut LocalTrapSink) {
local.trap_datas.drain(..).for_each(|(offset, trap_data)| {
self.trap_datas
.insert(current_func_offset + offset, trap_data);
}); });
} }
} }
pub struct LocalTrapSink {
trap_datas: Vec<(usize, TrapData)>,
}
impl LocalTrapSink {
pub fn new() -> Self {
LocalTrapSink { trap_datas: vec![] }
}
}
impl binemit::TrapSink for LocalTrapSink {
fn trap(&mut self, offset: u32, srcloc: SourceLoc, trapcode: TrapCode) {
self.trap_datas
.push((offset as usize, TrapData { trapcode, srcloc }));
}
}

View File

@ -1,5 +1,8 @@
use crate::call::HandlerData;
use crate::libcalls; use crate::libcalls;
use crate::relocation::{Reloc, RelocSink, Relocation, RelocationType, TrapSink, VmCall}; use crate::relocation::{
LocalTrapSink, Reloc, RelocSink, Relocation, RelocationType, TrapSink, VmCall,
};
use byteorder::{ByteOrder, LittleEndian}; use byteorder::{ByteOrder, LittleEndian};
use cranelift_codegen::{ir, isa, Context}; use cranelift_codegen::{ir, isa, Context};
use std::mem; use std::mem;
@ -17,17 +20,18 @@ use wasmer_runtime::{
pub struct FuncResolverBuilder { pub struct FuncResolverBuilder {
resolver: FuncResolver, resolver: FuncResolver,
relocations: Map<LocalFuncIndex, Vec<Relocation>>, relocations: Map<LocalFuncIndex, Vec<Relocation>>,
trap_sinks: Map<LocalFuncIndex, TrapSink>,
} }
impl FuncResolverBuilder { impl FuncResolverBuilder {
pub fn new( pub fn new(
isa: &isa::TargetIsa, isa: &isa::TargetIsa,
function_bodies: Map<LocalFuncIndex, ir::Function>, function_bodies: Map<LocalFuncIndex, ir::Function>,
) -> CompileResult<Self> { ) -> CompileResult<(Self, HandlerData)> {
let mut compiled_functions: Vec<Vec<u8>> = Vec::with_capacity(function_bodies.len()); let mut compiled_functions: Vec<Vec<u8>> = Vec::with_capacity(function_bodies.len());
let mut relocations = Map::with_capacity(function_bodies.len()); let mut relocations = Map::with_capacity(function_bodies.len());
let mut trap_sinks = Map::with_capacity(function_bodies.len());
let mut trap_sink = TrapSink::new();
let mut local_trap_sink = LocalTrapSink::new();
let mut ctx = Context::new(); let mut ctx = Context::new();
let mut total_size = 0; let mut total_size = 0;
@ -36,17 +40,20 @@ impl FuncResolverBuilder {
ctx.func = func; ctx.func = func;
let mut code_buf = Vec::new(); let mut code_buf = Vec::new();
let mut reloc_sink = RelocSink::new(); let mut reloc_sink = RelocSink::new();
let mut trap_sink = TrapSink::new();
ctx.compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut trap_sink) ctx.compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut local_trap_sink)
.map_err(|e| CompileError::InternalError { msg: e.to_string() })?; .map_err(|e| CompileError::InternalError { msg: e.to_string() })?;
ctx.clear(); ctx.clear();
// Clear the local trap sink and consolidate all trap info
// into a single location.
trap_sink.drain_local(total_size, &mut local_trap_sink);
// Round up each function's size to pointer alignment. // Round up each function's size to pointer alignment.
total_size += round_up(code_buf.len(), mem::size_of::<usize>()); total_size += round_up(code_buf.len(), mem::size_of::<usize>());
compiled_functions.push(code_buf); compiled_functions.push(code_buf);
relocations.push(reloc_sink.func_relocs); relocations.push(reloc_sink.func_relocs);
trap_sinks.push(trap_sink);
} }
let mut memory = Mmap::with_size(total_size) let mut memory = Mmap::with_size(total_size)
@ -84,11 +91,15 @@ impl FuncResolverBuilder {
previous_end = new_end; previous_end = new_end;
} }
Ok(Self { let handler_data = HandlerData::new(trap_sink, memory.as_ptr() as _, memory.size());
Ok((
Self {
resolver: FuncResolver { map, memory }, resolver: FuncResolver { map, memory },
relocations, relocations,
trap_sinks, },
}) handler_data,
))
} }
pub fn finalize(mut self) -> CompileResult<FuncResolver> { pub fn finalize(mut self) -> CompileResult<FuncResolver> {

View File

@ -4,7 +4,7 @@ use wasmer_runtime::{import::Imports, Instance};
fn main() { fn main() {
let mut instance = create_module_1(); let mut instance = create_module_1();
let result = instance.call("get-0", &[]); let result = instance.call("call-overwritten", &[]);
println!("result: {:?}", result); println!("result: {:?}", result);
} }
@ -20,14 +20,14 @@ fn main() {
fn create_module_1() -> Instance { fn create_module_1() -> Instance {
let module_str = r#"(module let module_str = r#"(module
(type (;0;) (func (result i32))) (type (;0;) (func (result i32)))
(import "spectest" "global_i32" (global (;0;) i32)) (type (;1;) (func))
(func (;0;) (type 0) (result i32) (table 10 anyfunc)
get_global 0) (elem (i32.const 0) 0)
(func (;1;) (type 0) (result i32) (func (;0;) (type 0) (i32.const 65))
get_global 1) (func (;1;) (type 1))
(global (;1;) i32 (get_global 0)) (func (export "call-overwritten") (type 0)
(export "get-0" (func 0)) (call_indirect (type 0) (i32.const 0))
(export "get-0-ref" (func 1))) ))
"#; "#;
let wasm_binary = wat2wasm(module_str.as_bytes()).expect("WAST not valid or malformed"); let wasm_binary = wat2wasm(module_str.as_bytes()).expect("WAST not valid or malformed");
let module = wasmer_runtime::compile(&wasm_binary[..], &CraneliftCompiler::new()) let module = wasmer_runtime::compile(&wasm_binary[..], &CraneliftCompiler::new())

View File

@ -80,8 +80,10 @@ impl PartialEq for LinkError {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RuntimeError { pub enum RuntimeError {
OutOfBoundsAccess { memory: MemoryIndex, addr: u32 }, OutOfBoundsAccess { memory: MemoryIndex, addr: u32 },
TableOutOfBounds { table: TableIndex },
IndirectCallSignature { table: TableIndex }, IndirectCallSignature { table: TableIndex },
IndirectCallToNull { table: TableIndex }, IndirectCallToNull { table: TableIndex },
IllegalArithmeticOperation,
Unknown { msg: String }, Unknown { msg: String },
} }