diff --git a/lib/environ/src/compilation.rs b/lib/environ/src/compilation.rs index d6d82d6644..87b63d0a9c 100644 --- a/lib/environ/src/compilation.rs +++ b/lib/environ/src/compilation.rs @@ -77,7 +77,8 @@ impl binemit::RelocSink for RelocSink { } impl RelocSink { - fn new() -> Self { + /// Return a new `RelocSink` instance. + pub fn new() -> Self { Self { func_relocs: Vec::new(), } diff --git a/lib/environ/src/lib.rs b/lib/environ/src/lib.rs index fb3b98c159..6e550c3c8f 100644 --- a/lib/environ/src/lib.rs +++ b/lib/environ/src/lib.rs @@ -45,7 +45,9 @@ mod module; mod tunables; mod vmcontext; -pub use compilation::{compile_module, Compilation, Relocation, RelocationTarget, Relocations}; +pub use compilation::{ + compile_module, Compilation, RelocSink, Relocation, RelocationTarget, Relocations, +}; pub use environ::{ModuleEnvironment, ModuleTranslation}; pub use module::{DataInitializer, Export, MemoryPlan, MemoryStyle, Module, TableElements}; pub use tunables::Tunables; diff --git a/lib/environ/src/module.rs b/lib/environ/src/module.rs index a12e528aca..1e9903804b 100644 --- a/lib/environ/src/module.rs +++ b/lib/environ/src/module.rs @@ -80,12 +80,11 @@ pub struct MemoryPlan { } impl MemoryPlan { - /// Draw up a plan for implementing `Memory`. + /// Draw up a plan for implementing a `Memory`. pub fn for_memory(memory: Memory, tunables: &Tunables) -> Self { Self { memory, style: MemoryStyle::for_memory(memory, tunables), - // fixme: saturate this offset_guard_size: tunables.offset_guard_size, } } diff --git a/lib/execute/Cargo.toml b/lib/execute/Cargo.toml index 306b8ad7f1..aad42d2590 100644 --- a/lib/execute/Cargo.toml +++ b/lib/execute/Cargo.toml @@ -13,10 +13,11 @@ readme = "README.md" cranelift-codegen = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" } cranelift-entity = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" } cranelift-wasm = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" } +cranelift-frontend = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" } wasmtime-environ = { path = "../environ" } region = "1.0.0" lazy_static = "1.2.0" -libc = "0.2.44" +libc = { version = "0.2.44", default-features = false } errno = "0.2.4" [build-dependencies] diff --git a/lib/execute/signalhandlers/SignalHandlers.cpp b/lib/execute/signalhandlers/SignalHandlers.cpp index 84327e9ea0..060645685b 100644 --- a/lib/execute/signalhandlers/SignalHandlers.cpp +++ b/lib/execute/signalhandlers/SignalHandlers.cpp @@ -400,6 +400,11 @@ HandleTrap(CONTEXT* context) RecordTrap(pc, codeSegment); + // Unwind calls longjmp, so it doesn't run the automatic + // sAlreadhHanldingTrap cleanups, so reset it manually before doing + // a longjmp. + sAlreadyHandlingTrap = false; + #if defined(__APPLE__) // Reroute the PC to run the Unwind function on the main stack after the // handler exits. This doesn't yet work for stack overflow traps, because diff --git a/lib/execute/src/code.rs b/lib/execute/src/code.rs new file mode 100644 index 0000000000..0a0b9d8a0b --- /dev/null +++ b/lib/execute/src/code.rs @@ -0,0 +1,72 @@ +//! Memory management for executable code. + +use mmap::Mmap; +use region; +use std::cmp; +use std::mem; +use std::slice; +use std::string::String; +use std::vec::Vec; + +/// Memory manager for executable code. +pub struct Code { + current: Mmap, + mmaps: Vec, + position: usize, + published: usize, +} + +impl Code { + /// Create a new `Code` instance. + pub fn new() -> Self { + Self { + current: Mmap::new(), + mmaps: Vec::new(), + position: 0, + published: 0, + } + } + + /// Allocate `size` bytes of memory which can be made executable later by + /// calling `publish()`. + /// TODO: alignment + pub fn allocate(&mut self, size: usize) -> Result<*mut u8, String> { + if self.current.len() - self.position < size { + self.mmaps.push(mem::replace( + &mut self.current, + Mmap::with_size(cmp::max(0x10000, size.next_power_of_two()))?, + )); + self.position = 0; + } + let old_position = self.position; + self.position += size; + Ok(self.current.as_mut_slice()[old_position..self.position].as_mut_ptr()) + } + + /// Allocate enough memory to hold a copy of `slice` and copy the data into it. + /// TODO: Reorganize the code that calls this to emit code directly into the + /// mmap region rather than into a Vec that we need to copy in. + pub fn allocate_copy_of_slice(&mut self, slice: &[u8]) -> Result<&mut [u8], String> { + let ptr = self.allocate(slice.len())?; + let new = unsafe { slice::from_raw_parts_mut(ptr, slice.len()) }; + new.copy_from_slice(slice); + Ok(new) + } + + /// Make all allocated memory executable. + pub fn publish(&mut self) { + self.mmaps + .push(mem::replace(&mut self.current, Mmap::new())); + self.position = 0; + + for m in &mut self.mmaps[self.published..] { + if !m.as_ptr().is_null() { + unsafe { + region::protect(m.as_mut_ptr(), m.len(), region::Protection::ReadExecute) + .expect("unable to make memory readonly"); + } + } + } + self.published = self.mmaps.len(); + } +} diff --git a/lib/execute/src/execute.rs b/lib/execute/src/execute.rs index 319a8529c8..11cc2284fd 100644 --- a/lib/execute/src/execute.rs +++ b/lib/execute/src/execute.rs @@ -1,19 +1,21 @@ +//! TODO: Move the contents of this file to other files, as "execute.rs" is +//! no longer a descriptive filename. + +use code::Code; use cranelift_codegen::binemit::Reloc; use cranelift_codegen::isa::TargetIsa; use cranelift_entity::{EntityRef, PrimaryMap}; -use cranelift_wasm::{DefinedFuncIndex, FuncIndex, MemoryIndex, TableIndex}; +use cranelift_wasm::{DefinedFuncIndex, MemoryIndex, TableIndex}; use instance::Instance; +use invoke::{invoke_by_index, InvokeOutcome}; use memory::LinearMemory; use region::protect; use region::Protection; -use signalhandlers::{ensure_eager_signal_handlers, ensure_full_signal_handlers, TrapContext}; -use std::mem::transmute; use std::ptr::{self, write_unaligned}; use std::string::String; use std::vec::Vec; -use traphandlers::call_wasm; use wasmtime_environ::{ - compile_module, Compilation, Export, Module, ModuleTranslation, Relocation, RelocationTarget, + compile_module, Compilation, Module, ModuleTranslation, Relocation, RelocationTarget, }; /// Executes a module that has been translated with the `wasmtime-environ` environment @@ -112,7 +114,7 @@ extern "C" fn current_memory(memory_index: u32, vmctx: *mut *mut u8) -> u32 { /// Create the VmCtx data structure for the JIT'd code to use. This must /// match the VmCtx layout in the environment. -fn make_vmctx(instance: &mut Instance, mem_base_addrs: &mut [*mut u8]) -> Vec<*mut u8> { +fn make_vmctx(instance: &mut Instance) -> Vec<*mut u8> { debug_assert!( instance.tables.len() <= 1, "non-default tables is not supported" @@ -128,7 +130,7 @@ fn make_vmctx(instance: &mut Instance, mem_base_addrs: &mut [*mut u8]) -> Vec<*m let mut vmctx = Vec::new(); vmctx.push(instance.globals.as_mut_ptr()); // FIXME: These need to be VMMemory now - vmctx.push(mem_base_addrs.as_mut_ptr() as *mut u8); + vmctx.push(instance.mem_base_addrs.as_mut_ptr() as *mut u8); // FIXME: These need to be VMTable now vmctx.push(default_table_ptr); vmctx.push(default_table_len as *mut u8); @@ -139,6 +141,8 @@ fn make_vmctx(instance: &mut Instance, mem_base_addrs: &mut [*mut u8]) -> Vec<*m /// prepares the execution context pub fn finish_instantiation( + code: &mut Code, + isa: &TargetIsa, module: &Module, compilation: &Compilation, instance: &mut Instance, @@ -164,67 +168,25 @@ pub fn finish_instantiation( } // Collect all memory base addresses and Vec. - let mut mem_base_addrs = instance + instance.mem_base_addrs = instance .memories .values_mut() .map(LinearMemory::base_addr) .collect::>(); - let mut vmctx = make_vmctx(instance, &mut mem_base_addrs); + let mut vmctx = make_vmctx(instance); if let Some(start_index) = module.start_func { - execute_by_index(module, compilation, &mut vmctx, start_index)?; + let result = invoke_by_index(code, isa, module, compilation, &mut vmctx, start_index, &[])?; + match result { + InvokeOutcome::Returned { values } => { + assert!(values.is_empty()); + } + InvokeOutcome::Trapped { message } => { + return Err(format!("start function trapped: {}", message)); + } + } } Ok(vmctx) } - -/// Jumps to the code region of memory and execute the exported function -pub fn execute( - module: &Module, - compilation: &Compilation, - vmctx: &mut Vec<*mut u8>, - function: &str, -) -> Result<(), String> { - let fn_index = match module.exports.get(function) { - Some(Export::Function(index)) => *index, - Some(_) => return Err(format!("exported item \"{}\" is not a function", function)), - None => return Err(format!("no export named \"{}\"", function)), - }; - - execute_by_index(module, compilation, vmctx, fn_index) -} - -fn execute_by_index( - module: &Module, - compilation: &Compilation, - vmctx: &mut Vec<*mut u8>, - fn_index: FuncIndex, -) -> Result<(), String> { - let code_buf = - &compilation.functions[module - .defined_func_index(fn_index) - .expect("imported start functions not supported yet")]; - - let mut traps = TrapContext { - triedToInstallSignalHandlers: false, - haveSignalHandlers: false, - }; - - // Rather than writing inline assembly to jump to the code region, we use the fact that - // the Rust ABI for calling a function with no arguments and no return values matches the one - // of the generated code. Thanks to this, we can transmute the code region into a first-class - // Rust function and call it. - unsafe { - // Ensure that our signal handlers are ready for action. - ensure_eager_signal_handlers(); - ensure_full_signal_handlers(&mut traps); - if !traps.haveSignalHandlers { - return Err("failed to install signal handlers".to_string()); - } - - let func = transmute::<_, fn(*const *mut u8)>(code_buf.as_ptr()); - call_wasm(|| func(vmctx.as_mut_ptr()))?; - } - Ok(()) -} diff --git a/lib/execute/src/instance.rs b/lib/execute/src/instance.rs index ddb95611fe..8cab4cd2fe 100644 --- a/lib/execute/src/instance.rs +++ b/lib/execute/src/instance.rs @@ -6,6 +6,7 @@ use cranelift_entity::EntityRef; use cranelift_entity::PrimaryMap; use cranelift_wasm::{GlobalIndex, MemoryIndex, TableIndex}; use memory::LinearMemory; +use std::string::String; use std::vec::Vec; use wasmtime_environ::{Compilation, DataInitializer, Module, TableElements}; @@ -20,6 +21,9 @@ pub struct Instance { /// WebAssembly global variable data. pub globals: Vec, + + /// Memory base address vector pointed to by vmctx. + pub mem_base_addrs: Vec<*mut u8>, } impl Instance { @@ -33,6 +37,7 @@ impl Instance { tables: PrimaryMap::new(), memories: PrimaryMap::new(), globals: Vec::new(), + mem_base_addrs: Vec::new(), }; result.instantiate_tables(module, compilation, &module.table_elements); result.instantiate_memories(module, data_initializers)?; diff --git a/lib/execute/src/invoke.rs b/lib/execute/src/invoke.rs new file mode 100644 index 0000000000..1519e90f04 --- /dev/null +++ b/lib/execute/src/invoke.rs @@ -0,0 +1,271 @@ +//! Support for invoking wasm functions from outside a wasm module. + +use code::Code; +use cranelift_codegen::ir::InstBuilder; +use cranelift_codegen::{binemit, ir, isa, Context}; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; +use cranelift_wasm::FuncIndex; +use signalhandlers::{ensure_eager_signal_handlers, ensure_full_signal_handlers, TrapContext}; +use std::mem; +use std::ptr; +use std::string::String; +use std::vec::Vec; +use traphandlers::call_wasm; +use wasmtime_environ::{Compilation, Export, Module, RelocSink}; + +/// A runtime value. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Value { + /// A runtime value with type i32. + I32(i32), + /// A runtime value with type i64. + I64(i64), + /// A runtime value with type f32. + F32(u32), + /// A runtime value with type f64. + F64(u64), +} + +impl Value { + /// Return the type of this `Value`. + pub fn value_type(self) -> ir::Type { + match self { + Value::I32(_) => ir::types::I32, + Value::I64(_) => ir::types::I64, + Value::F32(_) => ir::types::F32, + Value::F64(_) => ir::types::F64, + } + } + + /// Assuming this `Value` holds an `i32`, return that value. + pub fn unwrap_i32(self) -> i32 { + match self { + Value::I32(x) => x, + _ => panic!("unwrapping value of type {} as i32", self.value_type()), + } + } + + /// Assuming this `Value` holds an `i64`, return that value. + pub fn unwrap_i64(self) -> i64 { + match self { + Value::I64(x) => x, + _ => panic!("unwrapping value of type {} as i64", self.value_type()), + } + } + + /// Assuming this `Value` holds an `f32`, return that value. + pub fn unwrap_f32(self) -> u32 { + match self { + Value::F32(x) => x, + _ => panic!("unwrapping value of type {} as f32", self.value_type()), + } + } + + /// Assuming this `Value` holds an `f64`, return that value. + pub fn unwrap_f64(self) -> u64 { + match self { + Value::F64(x) => x, + _ => panic!("unwrapping value of type {} as f64", self.value_type()), + } + } +} + +/// The result of invoking a wasm function. +#[derive(Debug)] +pub enum InvokeOutcome { + /// The function returned normally. Its return values are provided. + Returned { + /// The return values. + values: Vec, + }, + /// A trap occurred while the function was executing. + Trapped { + /// The trap message. + message: String, + }, +} + +/// Jumps to the code region of memory and invoke the exported function +pub fn invoke( + code: &mut Code, + isa: &isa::TargetIsa, + module: &Module, + compilation: &Compilation, + vmctx: &mut Vec<*mut u8>, + function: &str, + args: &[Value], +) -> Result { + let fn_index = match module.exports.get(function) { + Some(Export::Function(index)) => *index, + Some(_) => return Err(format!("exported item \"{}\" is not a function", function)), + None => return Err(format!("no export named \"{}\"", function)), + }; + + invoke_by_index(code, isa, module, compilation, vmctx, fn_index, args) +} + +pub fn invoke_by_index( + code: &mut Code, + isa: &isa::TargetIsa, + module: &Module, + compilation: &Compilation, + vmctx: &mut Vec<*mut u8>, + fn_index: FuncIndex, + args: &[Value], +) -> Result { + let code_buf = + &compilation.functions[module + .defined_func_index(fn_index) + .expect("imported start functions not supported yet")]; + let sig = &module.signatures[module.functions[fn_index]]; + + let exec_code_buf = code.allocate_copy_of_slice(&code_buf)?.as_ptr(); + + // TODO: Move this out to be done once per thread rather than per call. + let mut traps = TrapContext { + triedToInstallSignalHandlers: false, + haveSignalHandlers: false, + }; + + // Rather than writing inline assembly to jump to the code region, we use the fact that + // the Rust ABI for calling a function with no arguments and no return values matches the one + // of the generated code. Thanks to this, we can transmute the code region into a first-class + // Rust function and call it. + // Ensure that our signal handlers are ready for action. + ensure_eager_signal_handlers(); + ensure_full_signal_handlers(&mut traps); + if !traps.haveSignalHandlers { + return Err("failed to install signal handlers".to_string()); + } + + call_through_wrapper( + code, + isa, + exec_code_buf as usize, + vmctx.as_ptr() as usize, + args, + &sig, + ) +} + +fn call_through_wrapper( + code: &mut Code, + isa: &isa::TargetIsa, + callee: usize, + vmctx: usize, + args: &[Value], + sig: &ir::Signature, +) -> Result { + for (index, value) in args.iter().enumerate() { + assert_eq!(value.value_type(), sig.params[index].value_type); + } + + let wrapper_sig = ir::Signature::new(isa.frontend_config().default_call_conv); + let mut context = Context::new(); + context.func = ir::Function::with_name_signature(ir::ExternalName::user(0, 0), wrapper_sig); + + let value_size = 8; + let mut results_vec = Vec::new(); + results_vec.resize(sig.returns.len(), 0i64); + + let mut fn_builder_ctx = FunctionBuilderContext::new(); + { + let mut builder = FunctionBuilder::new(&mut context.func, &mut fn_builder_ctx); + let block0 = builder.create_ebb(); + + builder.append_ebb_params_for_function_params(block0); + + builder.switch_to_block(block0); + builder.seal_block(block0); + + let mut callee_args = Vec::new(); + let pointer_type = isa.pointer_type(); + + let callee_value = builder.ins().iconst(pointer_type, callee as i64); + + for value in args { + match value { + Value::I32(i) => { + callee_args.push(builder.ins().iconst(ir::types::I32, i64::from(*i))) + } + Value::I64(i) => callee_args.push(builder.ins().iconst(ir::types::I64, *i)), + Value::F32(i) => callee_args.push( + builder + .ins() + .f32const(ir::immediates::Ieee32::with_bits(*i)), + ), + Value::F64(i) => callee_args.push( + builder + .ins() + .f64const(ir::immediates::Ieee64::with_bits(*i)), + ), + } + } + + let vmctx_value = builder.ins().iconst(pointer_type, vmctx as i64); + callee_args.push(vmctx_value); + + let new_sig = builder.import_signature(sig.clone()); + + // TODO: It's possible to make this a direct call. We just need Cranelift + // to support functions declared with an immediate integer address. + let call = builder + .ins() + .call_indirect(new_sig, callee_value, &callee_args); + + let results = builder.func.dfg.inst_results(call).to_vec(); + + let results_vec_value = builder + .ins() + .iconst(pointer_type, results_vec.as_ptr() as i64); + + let mut mflags = ir::MemFlags::new(); + mflags.set_notrap(); + mflags.set_aligned(); + for (i, r) in results.iter().enumerate() { + builder + .ins() + .store(mflags, *r, results_vec_value, (i * value_size) as i32); + } + + builder.ins().return_(&[]); + } + + let mut code_buf: Vec = Vec::new(); + let mut reloc_sink = RelocSink::new(); + let mut trap_sink = binemit::NullTrapSink {}; + context + .compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut trap_sink) + .map_err(|e| e.to_string())?; + assert!(reloc_sink.func_relocs.is_empty()); + + let exec_code_buf = code.allocate_copy_of_slice(&code_buf)?.as_ptr(); + code.publish(); + + let func = unsafe { mem::transmute::<_, fn()>(exec_code_buf) }; + + Ok(match call_wasm(func) { + Ok(()) => { + let mut values = Vec::with_capacity(sig.returns.len()); + + for (index, abi_param) in sig.returns.iter().enumerate() { + let v = unsafe { + let ptr = results_vec.as_ptr().add(index * value_size); + + match abi_param.value_type { + ir::types::I32 => Value::I32(ptr::read(ptr as *const i32)), + ir::types::I64 => Value::I64(ptr::read(ptr as *const i64)), + ir::types::F32 => Value::F32(ptr::read(ptr as *const u32)), + ir::types::F64 => Value::F64(ptr::read(ptr as *const u64)), + other => panic!("unsupported value type {:?}", other), + } + }; + + values.push(v); + } + + InvokeOutcome::Returned { values } + } + Err(message) => InvokeOutcome::Trapped { message }, + }) +} diff --git a/lib/execute/src/lib.rs b/lib/execute/src/lib.rs index ae73241637..0abbc0c061 100644 --- a/lib/execute/src/lib.rs +++ b/lib/execute/src/lib.rs @@ -29,6 +29,7 @@ extern crate cranelift_codegen; extern crate cranelift_entity; +extern crate cranelift_frontend; extern crate cranelift_wasm; extern crate errno; extern crate region; @@ -40,14 +41,19 @@ extern crate alloc; extern crate lazy_static; extern crate libc; +mod code; mod execute; mod instance; +mod invoke; mod memory; +mod mmap; mod signalhandlers; mod traphandlers; -pub use execute::{compile_and_link_module, execute, finish_instantiation}; +pub use code::Code; +pub use execute::{compile_and_link_module, finish_instantiation}; pub use instance::Instance; +pub use invoke::{invoke, InvokeOutcome, Value}; pub use traphandlers::{call_wasm, LookupCodeSegment, RecordTrap, Unwind}; #[cfg(not(feature = "std"))] diff --git a/lib/execute/src/memory.rs b/lib/execute/src/memory.rs index ff515a28e6..51758183da 100644 --- a/lib/execute/src/memory.rs +++ b/lib/execute/src/memory.rs @@ -1,108 +1,17 @@ -use errno; -use libc; +//! Memory management for linear memory. + +use mmap::Mmap; use region; use std::fmt; -use std::mem; -use std::ptr; -use std::slice; +use std::string::String; use wasmtime_environ::{MemoryPlan, MemoryStyle, WASM_MAX_PAGES, WASM_PAGE_SIZE}; -/// Round `size` up to the nearest multiple of `page_size`. -fn round_up_to_page_size(size: usize, page_size: usize) -> usize { - (size + (page_size - 1)) & !(page_size - 1) -} - -/// A simple struct consisting of a page-aligned pointer to page-aligned -/// and initially-zeroed memory and a length. -struct PtrLen { - ptr: *mut u8, - len: usize, -} - -impl PtrLen { - /// Create a new `PtrLen` pointing to at least `size` bytes of memory, - /// suitably sized and aligned for memory protection. - #[cfg(not(target_os = "windows"))] - fn with_size(size: usize) -> Result { - let page_size = region::page::size(); - let alloc_size = round_up_to_page_size(size, page_size); - unsafe { - let ptr = libc::mmap( - ptr::null_mut(), - alloc_size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANON, - -1, - 0, - ); - if mem::transmute::<_, isize>(ptr) != -1isize { - Ok(Self { - ptr: ptr as *mut u8, - len: alloc_size, - }) - } else { - Err(errno::errno().to_string()) - } - } - } - - #[cfg(target_os = "windows")] - fn with_size(size: usize) -> Result { - use winapi::um::memoryapi::VirtualAlloc; - use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE}; - - let page_size = region::page::size(); - - // VirtualAlloc always rounds up to the next multiple of the page size - let ptr = unsafe { - VirtualAlloc( - ptr::null_mut(), - size, - MEM_COMMIT | MEM_RESERVE, - PAGE_READWRITE, - ) - }; - if !ptr.is_null() { - Ok(Self { - ptr: ptr as *mut u8, - len: round_up_to_page_size(size, page_size), - }) - } else { - Err(errno::errno().to_string()) - } - } - - fn as_slice(&self) -> &[u8] { - unsafe { slice::from_raw_parts(self.ptr, self.len) } - } - - fn as_mut_slice(&mut self) -> &mut [u8] { - unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } - } -} - -impl Drop for PtrLen { - #[cfg(not(target_os = "windows"))] - fn drop(&mut self) { - let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; - assert_eq!(r, 0); - } - - #[cfg(target_os = "windows")] - fn drop(&mut self) { - use winapi::um::memoryapi::VirtualFree; - use winapi::um::winnt::MEM_RELEASE; - let r = unsafe { VirtualFree(self.ptr, self.len, MEM_RELEASE) }; - assert_eq!(r, 0); - } -} - /// A linear memory instance. /// /// This linear memory has a stable base address and at the same time allows /// for dynamical growing. pub struct LinearMemory { - ptrlen: PtrLen, + mmap: Mmap, current: u32, maximum: Option, offset_guard_size: usize, @@ -132,19 +41,19 @@ impl LinearMemory { let unmapped_bytes = unmapped_pages * WASM_PAGE_SIZE as usize; let inaccessible_bytes = unmapped_bytes + offset_guard_bytes; - let ptrlen = PtrLen::with_size(request_bytes)?; + let mmap = Mmap::with_size(request_bytes)?; // Make the unmapped and offset-guard pages inaccessible. unsafe { region::protect( - ptrlen.ptr.add(mapped_bytes), + mmap.as_ptr().add(mapped_bytes), inaccessible_bytes, - region::Protection::Read, - ).expect("unable to make memory readonly"); + region::Protection::None, + ).expect("unable to make memory inaccessible"); } Ok(Self { - ptrlen, + mmap, current: plan.memory.minimum, maximum: plan.memory.maximum, offset_guard_size: offset_guard_bytes, @@ -153,13 +62,13 @@ impl LinearMemory { /// Returns an base address of this linear memory. pub fn base_addr(&mut self) -> *mut u8 { - self.ptrlen.ptr + self.mmap.as_mut_ptr() } /// Returns a number of allocated wasm pages. pub fn current_size(&self) -> u32 { - assert_eq!(self.ptrlen.len % WASM_PAGE_SIZE as usize, 0); - let num_pages = self.ptrlen.len / WASM_PAGE_SIZE as usize; + assert_eq!(self.mmap.len() % WASM_PAGE_SIZE as usize, 0); + let num_pages = self.mmap.len() / WASM_PAGE_SIZE as usize; assert_eq!(num_pages as u32 as usize, num_pages); num_pages as u32 } @@ -193,29 +102,29 @@ impl LinearMemory { let new_bytes = new_pages as usize * WASM_PAGE_SIZE as usize; - if new_bytes > self.ptrlen.len { + if new_bytes > self.mmap.len() { // If we have no maximum, this is a "dynamic" heap, and it's allowed to move. assert!(self.maximum.is_none()); let mapped_pages = self.current as usize; let mapped_bytes = mapped_pages * WASM_PAGE_SIZE as usize; let guard_bytes = self.offset_guard_size; - let mut new_ptrlen = PtrLen::with_size(new_bytes).ok()?; + let mut new_mmap = Mmap::with_size(new_bytes).ok()?; // Make the offset-guard pages inaccessible. unsafe { region::protect( - new_ptrlen.ptr.add(mapped_bytes), + new_mmap.as_ptr().add(mapped_bytes), guard_bytes, region::Protection::Read, ).expect("unable to make memory readonly"); } - new_ptrlen + new_mmap .as_mut_slice() - .copy_from_slice(self.ptrlen.as_slice()); + .copy_from_slice(self.mmap.as_slice()); - self.ptrlen = new_ptrlen; + self.mmap = new_mmap; } self.current = new_pages; @@ -235,25 +144,12 @@ impl fmt::Debug for LinearMemory { impl AsRef<[u8]> for LinearMemory { fn as_ref(&self) -> &[u8] { - self.ptrlen.as_slice() + self.mmap.as_slice() } } impl AsMut<[u8]> for LinearMemory { fn as_mut(&mut self) -> &mut [u8] { - self.ptrlen.as_mut_slice() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_round_up_to_page_size() { - assert_eq!(round_up_to_page_size(0, 4096), 0); - assert_eq!(round_up_to_page_size(1, 4096), 4096); - assert_eq!(round_up_to_page_size(4096, 4096), 4096); - assert_eq!(round_up_to_page_size(4097, 4096), 8192); + self.mmap.as_mut_slice() } } diff --git a/lib/execute/src/mmap.rs b/lib/execute/src/mmap.rs new file mode 100644 index 0000000000..9969452469 --- /dev/null +++ b/lib/execute/src/mmap.rs @@ -0,0 +1,136 @@ +//! Low-level abstraction for allocating and managing zero-filled pages +//! of memory. + +use errno; +use libc; +use region; +use std::mem; +use std::ptr; +use std::slice; +use std::string::String; + +/// Round `size` up to the nearest multiple of `page_size`. +fn round_up_to_page_size(size: usize, page_size: usize) -> usize { + (size + (page_size - 1)) & !(page_size - 1) +} + +/// A simple struct consisting of a page-aligned pointer to page-aligned +/// and initially-zeroed memory and a length. +pub struct Mmap { + ptr: *mut u8, + len: usize, +} + +impl Mmap { + pub fn new() -> Self { + Self { + ptr: ptr::null_mut(), + len: 0, + } + } + + /// Create a new `Mmap` pointing to at least `size` bytes of memory, + /// suitably sized and aligned for memory protection. + #[cfg(not(target_os = "windows"))] + pub fn with_size(size: usize) -> Result { + let page_size = region::page::size(); + let alloc_size = round_up_to_page_size(size, page_size); + unsafe { + let ptr = libc::mmap( + ptr::null_mut(), + alloc_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ); + if mem::transmute::<_, isize>(ptr) != -1isize { + Ok(Self { + ptr: ptr as *mut u8, + len: alloc_size, + }) + } else { + Err(errno::errno().to_string()) + } + } + } + + #[cfg(target_os = "windows")] + pub fn with_size(size: usize) -> Result { + use winapi::um::memoryapi::VirtualAlloc; + use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE}; + + let page_size = region::page::size(); + + // VirtualAlloc always rounds up to the next multiple of the page size + let ptr = unsafe { + VirtualAlloc( + ptr::null_mut(), + size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + ) + }; + if !ptr.is_null() { + Ok(Self { + ptr: ptr as *mut u8, + len: round_up_to_page_size(size, page_size), + }) + } else { + Err(errno::errno().to_string()) + } + } + + pub fn as_slice(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.ptr, self.len) } + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } + } + + pub fn as_ptr(&self) -> *const u8 { + self.ptr + } + + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.ptr + } + + pub fn len(&self) -> usize { + self.len + } +} + +impl Drop for Mmap { + #[cfg(not(target_os = "windows"))] + fn drop(&mut self) { + if !self.ptr.is_null() { + let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; + assert_eq!(r, 0, "munmap failed: {}", errno::errno()); + } + } + + #[cfg(target_os = "windows")] + fn drop(&mut self) { + if !self.ptr.is_null() { + use winapi::um::memoryapi::VirtualFree; + use winapi::um::winnt::MEM_RELEASE; + let r = unsafe { VirtualFree(self.ptr, self.len, MEM_RELEASE) }; + assert_eq!(r, 0); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_round_up_to_page_size() { + assert_eq!(round_up_to_page_size(0, 4096), 0); + assert_eq!(round_up_to_page_size(1, 4096), 4096); + assert_eq!(round_up_to_page_size(4096, 4096), 4096); + assert_eq!(round_up_to_page_size(4097, 4096), 8192); + } +} diff --git a/lib/execute/src/traphandlers.rs b/lib/execute/src/traphandlers.rs index bc84c2309e..28f3861e17 100644 --- a/lib/execute/src/traphandlers.rs +++ b/lib/execute/src/traphandlers.rs @@ -6,6 +6,7 @@ use signalhandlers::{jmp_buf, CodeSegment}; use std::cell::{Cell, RefCell}; use std::mem; use std::ptr; +use std::string::String; // Currently we uset setjmp/longjmp to unwind out of a signal handler // and back to the point where WebAssembly was called (via `call_wasm`). diff --git a/src/main.rs b/src/main.rs index 4936e9bf7f..e7162a1cbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,9 +59,9 @@ use std::io::prelude::*; use std::io::stdout; use std::path::Path; use std::path::PathBuf; -use std::process::{exit, Command}; +use std::process::exit; use wasmtime_environ::{Module, ModuleEnvironment, Tunables}; -use wasmtime_execute::{compile_and_link_module, execute, finish_instantiation, Instance}; +use wasmtime_execute::{compile_and_link_module, finish_instantiation, invoke, Code, Instance}; static LOG_FILENAME_PREFIX: &str = "cranelift.dbg."; @@ -157,6 +157,8 @@ fn handle_module(args: &Args, path: PathBuf, isa: &TargetIsa) -> Result<(), Stri let translation = environ.translate(&data).map_err(|e| e.to_string())?; + let mut code = Code::new(); + let instance = match compile_and_link_module(isa, &translation, &imports_resolver) { Ok(compilation) => { let mut instance = Instance::new( @@ -165,11 +167,24 @@ fn handle_module(args: &Args, path: PathBuf, isa: &TargetIsa) -> Result<(), Stri &translation.lazy.data_initializers, )?; - let mut context = - finish_instantiation(&translation.module, &compilation, &mut instance)?; + let mut context = finish_instantiation( + &mut code, + isa, + &translation.module, + &compilation, + &mut instance, + )?; if let Some(ref f) = args.flag_function { - execute(&translation.module, &compilation, &mut context, &f)?; + invoke( + &mut code, + isa, + &translation.module, + &compilation, + &mut context, + &f, + &[], + )?; } instance diff --git a/tests/wast.rs b/tests/wast.rs new file mode 100644 index 0000000000..e1d6d89240 --- /dev/null +++ b/tests/wast.rs @@ -0,0 +1,297 @@ +extern crate cranelift_codegen; +extern crate wabt; +extern crate wasmtime_environ; +extern crate wasmtime_execute; + +use cranelift_codegen::settings::Configurable; +use cranelift_codegen::{isa, settings}; +use std::collections::HashMap; +use std::fs; +use std::io; +use std::io::Read; +use std::path::Path; +use std::str; +use wabt::script::{self, Action, Command, CommandKind, ScriptParser}; +use wasmtime_environ::{Compilation, Module, ModuleEnvironment, Tunables}; +use wasmtime_execute::{ + compile_and_link_module, finish_instantiation, invoke, Code, Instance, InvokeOutcome, Value, +}; + +struct InstanceWorld { + module: Module, + context: Vec<*mut u8>, + // FIXME + #[allow(dead_code)] + instance: Instance, + compilation: Compilation, +} + +impl InstanceWorld { + fn new(code: &mut Code, isa: &isa::TargetIsa, data: &[u8]) -> Result { + let mut module = Module::new(); + let tunables = Tunables::default(); + let (context, instance, compilation) = { + let translation = { + let environ = ModuleEnvironment::new(isa, &mut module, tunables); + + environ.translate(&data).map_err(|e| e.to_string())? + }; + + let imports_resolver = |_env: &str, _function: &str| None; + + let compilation = compile_and_link_module(isa, &translation, &imports_resolver)?; + let mut instance = Instance::new( + translation.module, + &compilation, + &translation.lazy.data_initializers, + )?; + + ( + finish_instantiation(code, isa, &translation.module, &compilation, &mut instance)?, + instance, + compilation, + ) + }; + + Ok(Self { + module, + context, + instance, + compilation, + }) + } + + fn invoke( + &mut self, + code: &mut Code, + isa: &isa::TargetIsa, + f: &str, + args: &[Value], + ) -> Result { + invoke( + code, + isa, + &self.module, + &self.compilation, + &mut self.context, + &f, + args, + ).map_err(|e| e.to_string()) + } +} + +fn translate(code: &mut Code, isa: &isa::TargetIsa, data: &[u8]) -> Result { + InstanceWorld::new(code, isa, data) +} + +struct Instances { + current: Option, + namespace: HashMap, +} + +impl Instances { + fn new() -> Self { + Self { + current: None, + namespace: HashMap::new(), + } + } + + fn unnamed(&mut self, instance: InstanceWorld) { + self.current = Some(instance); + } + + fn named(&mut self, name: String, instance: InstanceWorld) { + self.namespace.insert(name, instance); + } + + fn perform_action( + &mut self, + code: &mut Code, + isa: &isa::TargetIsa, + action: Action, + ) -> InvokeOutcome { + match action { + Action::Invoke { + module, + field, + args, + } => { + let mut value_args = Vec::new(); + for a in args { + value_args.push(match a { + script::Value::I32(i) => Value::I32(i), + script::Value::I64(i) => Value::I64(i), + script::Value::F32(i) => Value::F32(i.to_bits()), + script::Value::F64(i) => Value::F64(i.to_bits()), + }); + } + match module { + None => match self.current { + None => panic!("invoke performed with no module present"), + Some(ref mut instance_world) => instance_world + .invoke(code, isa, &field, &value_args) + .expect(&format!("error invoking {} in current module", field)), + }, + Some(name) => self + .namespace + .get_mut(&name) + .expect(&format!("module {} not declared", name)) + .invoke(code, isa, &field, &value_args) + .expect(&format!("error invoking {} in module {}", field, name)), + } + } + _ => panic!("unsupported action {:?}", action), + } + } +} + +#[test] +fn spec_core() { + let mut flag_builder = settings::builder(); + flag_builder.enable("enable_verifier").unwrap(); + + let isa_builder = cranelift_native::builder().unwrap_or_else(|_| { + panic!("host machine is not a supported target"); + }); + let isa = isa_builder.finish(settings::Flags::new(flag_builder)); + + let mut paths: Vec<_> = fs::read_dir("tests/wast") + .unwrap() + .map(|r| r.unwrap()) + .filter(|p| { + // Ignore files starting with `.`, which could be editor temporary files + if let Some(stem) = p.path().file_stem() { + if let Some(stemstr) = stem.to_str() { + return !stemstr.starts_with('.'); + } + } + false + }).collect(); + paths.sort_by_key(|dir| dir.path()); + for path in paths { + let path = path.path(); + let source = read_to_end(&path).unwrap(); + test_wast(&path, &*isa, &source); + } +} + +#[cfg(test)] +fn read_to_end(path: &Path) -> Result, io::Error> { + let mut buf: Vec = Vec::new(); + let mut file = fs::File::open(path)?; + file.read_to_end(&mut buf)?; + Ok(buf) +} + +#[cfg(test)] +fn test_wast(path: &Path, isa: &isa::TargetIsa, wast: &[u8]) { + println!("Testing {}", path.display()); + + let mut parser = ScriptParser::from_str(str::from_utf8(wast).unwrap()).unwrap(); + let mut instances = Instances::new(); + let mut code = Code::new(); + + while let Some(Command { kind, line }) = parser.next().unwrap() { + match kind { + CommandKind::Module { module, name } => { + if let Some(name) = name { + instances.named( + name, + translate(&mut code, &*isa, &module.clone().into_vec()).unwrap(), + ); + } + + instances.unnamed(translate(&mut code, &*isa, &module.clone().into_vec()).unwrap()); + } + CommandKind::PerformAction(action) => { + match instances.perform_action(&mut code, &*isa, action) { + InvokeOutcome::Returned { .. } => {} + InvokeOutcome::Trapped { message } => { + panic!("{}:{}: a trap occurred: {}", path.display(), line, message); + } + } + } + CommandKind::AssertReturn { action, expected } => { + match instances.perform_action(&mut code, &*isa, action) { + InvokeOutcome::Returned { values } => { + for (v, e) in values.iter().zip(expected.iter()) { + match *e { + script::Value::I32(x) => { + assert_eq!(x, v.unwrap_i32(), "at {}:{}", path.display(), line) + } + script::Value::I64(x) => { + assert_eq!(x, v.unwrap_i64(), "at {}:{}", path.display(), line) + } + script::Value::F32(x) => assert_eq!( + x.to_bits(), + v.unwrap_f32(), + "at {}:{}", + path.display(), + line + ), + script::Value::F64(x) => assert_eq!( + x.to_bits(), + v.unwrap_f64(), + "at {}:{}", + path.display(), + line + ), + }; + } + } + InvokeOutcome::Trapped { message } => { + panic!( + "{}:{}: expected normal return, but a trap occurred: {}", + path.display(), + line, + message + ); + } + } + } + CommandKind::AssertTrap { action, message } => { + match instances.perform_action(&mut code, &*isa, action) { + InvokeOutcome::Returned { values } => panic!( + "{}:{}: expected trap, but invoke returned with {:?}", + path.display(), + line, + values + ), + InvokeOutcome::Trapped { + message: trap_message, + } => { + println!( + "{}:{}: TODO: Check the trap message: expected {}, got {}", + path.display(), + line, + message, + trap_message + ); + } + } + } + CommandKind::AssertExhaustion { action } => { + match instances.perform_action(&mut code, &*isa, action) { + InvokeOutcome::Returned { values } => panic!( + "{}:{}: expected exhaustion, but invoke returned with {:?}", + path.display(), + line, + values + ), + InvokeOutcome::Trapped { message } => { + println!( + "{}:{}: TODO: Check the exhaustion message: {}", + path.display(), + line, + message + ); + } + } + } + command => { + println!("{}:{}: TODO: implement {:?}", path.display(), line, command); + } + } + } +} diff --git a/tests/wast/misc_traps.wast b/tests/wast/misc_traps.wast new file mode 100644 index 0000000000..96acf58bfe --- /dev/null +++ b/tests/wast/misc_traps.wast @@ -0,0 +1,67 @@ +(module + (memory 1 1) + (func (export "load_oob") + i32.const 65536 + i32.load + drop + ) +) + +(assert_trap (invoke "load_oob") "out of bounds memory access") +(assert_trap (invoke "load_oob") "out of bounds memory access") + +(module + (memory 1 1) + (func (export "store_oob") + i32.const 65536 + i32.const 65536 + i32.store + ) +) + +(assert_trap (invoke "store_oob") "out of bounds memory access") +(assert_trap (invoke "store_oob") "out of bounds memory access") + +(module + (memory 0 0) + (func (export "load_oob_0") + i32.const 0 + i32.load + drop + ) +) + +(assert_trap (invoke "load_oob_0") "out of bounds memory access") +(assert_trap (invoke "load_oob_0") "out of bounds memory access") + +(module + (memory 0 0) + (func (export "store_oob_0") + i32.const 0 + i32.const 0 + i32.store + ) +) + +(assert_trap (invoke "store_oob_0") "out of bounds memory access") +(assert_trap (invoke "store_oob_0") "out of bounds memory access") + +(module + (func (export "divbyzero") (result i32) + i32.const 1 + i32.const 0 + i32.div_s + ) +) + +(assert_trap (invoke "divbyzero") "integer divide by zero") +(assert_trap (invoke "divbyzero") "integer divide by zero") + +(module + (func (export "unreachable") + (unreachable) + ) +) + +(assert_trap (invoke "unreachable") "unreachable") +(assert_trap (invoke "unreachable") "unreachable") diff --git a/tests/wast/stack_overflow.wast b/tests/wast/stack_overflow.wast new file mode 100644 index 0000000000..baf4c98a7a --- /dev/null +++ b/tests/wast/stack_overflow.wast @@ -0,0 +1,26 @@ +(module + (func $foo + (call $foo) + ) + (func (export "stack_overflow") + (call $foo) + ) +) + +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") + +(module + (func $foo + (call $bar) + ) + (func $bar + (call $foo) + ) + (func (export "stack_overflow") + (call $foo) + ) +) + +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted")