Start a wast testing harness and add some tests.

This implements a minimal wast testing harness in tests/wast.rs, which
runs the wast tests under tests/wast.

It also adds tests for trapping in a variety of ways, and fixes several
bugs exposed by those tests.
This commit is contained in:
Dan Gohman
2018-11-29 13:40:39 -08:00
parent a6b54330c0
commit 8dbd4b8d7c
17 changed files with 958 additions and 196 deletions

View File

@@ -77,7 +77,8 @@ impl binemit::RelocSink for RelocSink {
} }
impl RelocSink { impl RelocSink {
fn new() -> Self { /// Return a new `RelocSink` instance.
pub fn new() -> Self {
Self { Self {
func_relocs: Vec::new(), func_relocs: Vec::new(),
} }

View File

@@ -45,7 +45,9 @@ mod module;
mod tunables; mod tunables;
mod vmcontext; 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 environ::{ModuleEnvironment, ModuleTranslation};
pub use module::{DataInitializer, Export, MemoryPlan, MemoryStyle, Module, TableElements}; pub use module::{DataInitializer, Export, MemoryPlan, MemoryStyle, Module, TableElements};
pub use tunables::Tunables; pub use tunables::Tunables;

View File

@@ -80,12 +80,11 @@ pub struct MemoryPlan {
} }
impl 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 { pub fn for_memory(memory: Memory, tunables: &Tunables) -> Self {
Self { Self {
memory, memory,
style: MemoryStyle::for_memory(memory, tunables), style: MemoryStyle::for_memory(memory, tunables),
// fixme: saturate this
offset_guard_size: tunables.offset_guard_size, offset_guard_size: tunables.offset_guard_size,
} }
} }

View File

@@ -13,10 +13,11 @@ readme = "README.md"
cranelift-codegen = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" } 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-entity = { git = "https://github.com/sunfishcode/cranelift.git", branch = "guard-offset" }
cranelift-wasm = { 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" } wasmtime-environ = { path = "../environ" }
region = "1.0.0" region = "1.0.0"
lazy_static = "1.2.0" lazy_static = "1.2.0"
libc = "0.2.44" libc = { version = "0.2.44", default-features = false }
errno = "0.2.4" errno = "0.2.4"
[build-dependencies] [build-dependencies]

View File

@@ -400,6 +400,11 @@ HandleTrap(CONTEXT* context)
RecordTrap(pc, codeSegment); 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__) #if defined(__APPLE__)
// Reroute the PC to run the Unwind function on the main stack after the // 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 // handler exits. This doesn't yet work for stack overflow traps, because

72
lib/execute/src/code.rs Normal file
View File

@@ -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<Mmap>,
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();
}
}

View File

@@ -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::binemit::Reloc;
use cranelift_codegen::isa::TargetIsa; use cranelift_codegen::isa::TargetIsa;
use cranelift_entity::{EntityRef, PrimaryMap}; use cranelift_entity::{EntityRef, PrimaryMap};
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, MemoryIndex, TableIndex}; use cranelift_wasm::{DefinedFuncIndex, MemoryIndex, TableIndex};
use instance::Instance; use instance::Instance;
use invoke::{invoke_by_index, InvokeOutcome};
use memory::LinearMemory; use memory::LinearMemory;
use region::protect; use region::protect;
use region::Protection; 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::ptr::{self, write_unaligned};
use std::string::String; use std::string::String;
use std::vec::Vec; use std::vec::Vec;
use traphandlers::call_wasm;
use wasmtime_environ::{ 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 /// 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 /// Create the VmCtx data structure for the JIT'd code to use. This must
/// match the VmCtx layout in the environment. /// 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!( debug_assert!(
instance.tables.len() <= 1, instance.tables.len() <= 1,
"non-default tables is not supported" "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(); let mut vmctx = Vec::new();
vmctx.push(instance.globals.as_mut_ptr()); vmctx.push(instance.globals.as_mut_ptr());
// FIXME: These need to be VMMemory now // 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 // FIXME: These need to be VMTable now
vmctx.push(default_table_ptr); vmctx.push(default_table_ptr);
vmctx.push(default_table_len as *mut u8); 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 /// prepares the execution context
pub fn finish_instantiation( pub fn finish_instantiation(
code: &mut Code,
isa: &TargetIsa,
module: &Module, module: &Module,
compilation: &Compilation, compilation: &Compilation,
instance: &mut Instance, instance: &mut Instance,
@@ -164,67 +168,25 @@ pub fn finish_instantiation(
} }
// Collect all memory base addresses and Vec. // Collect all memory base addresses and Vec.
let mut mem_base_addrs = instance instance.mem_base_addrs = instance
.memories .memories
.values_mut() .values_mut()
.map(LinearMemory::base_addr) .map(LinearMemory::base_addr)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut vmctx = make_vmctx(instance, &mut mem_base_addrs); let mut vmctx = make_vmctx(instance);
if let Some(start_index) = module.start_func { 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) 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(())
}

View File

@@ -6,6 +6,7 @@ use cranelift_entity::EntityRef;
use cranelift_entity::PrimaryMap; use cranelift_entity::PrimaryMap;
use cranelift_wasm::{GlobalIndex, MemoryIndex, TableIndex}; use cranelift_wasm::{GlobalIndex, MemoryIndex, TableIndex};
use memory::LinearMemory; use memory::LinearMemory;
use std::string::String;
use std::vec::Vec; use std::vec::Vec;
use wasmtime_environ::{Compilation, DataInitializer, Module, TableElements}; use wasmtime_environ::{Compilation, DataInitializer, Module, TableElements};
@@ -20,6 +21,9 @@ pub struct Instance {
/// WebAssembly global variable data. /// WebAssembly global variable data.
pub globals: Vec<u8>, pub globals: Vec<u8>,
/// Memory base address vector pointed to by vmctx.
pub mem_base_addrs: Vec<*mut u8>,
} }
impl Instance { impl Instance {
@@ -33,6 +37,7 @@ impl Instance {
tables: PrimaryMap::new(), tables: PrimaryMap::new(),
memories: PrimaryMap::new(), memories: PrimaryMap::new(),
globals: Vec::new(), globals: Vec::new(),
mem_base_addrs: Vec::new(),
}; };
result.instantiate_tables(module, compilation, &module.table_elements); result.instantiate_tables(module, compilation, &module.table_elements);
result.instantiate_memories(module, data_initializers)?; result.instantiate_memories(module, data_initializers)?;

271
lib/execute/src/invoke.rs Normal file
View File

@@ -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<Value>,
},
/// 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<InvokeOutcome, 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)),
};
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<InvokeOutcome, String> {
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<InvokeOutcome, String> {
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<u8> = 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 },
})
}

View File

@@ -29,6 +29,7 @@
extern crate cranelift_codegen; extern crate cranelift_codegen;
extern crate cranelift_entity; extern crate cranelift_entity;
extern crate cranelift_frontend;
extern crate cranelift_wasm; extern crate cranelift_wasm;
extern crate errno; extern crate errno;
extern crate region; extern crate region;
@@ -40,14 +41,19 @@ extern crate alloc;
extern crate lazy_static; extern crate lazy_static;
extern crate libc; extern crate libc;
mod code;
mod execute; mod execute;
mod instance; mod instance;
mod invoke;
mod memory; mod memory;
mod mmap;
mod signalhandlers; mod signalhandlers;
mod traphandlers; 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 instance::Instance;
pub use invoke::{invoke, InvokeOutcome, Value};
pub use traphandlers::{call_wasm, LookupCodeSegment, RecordTrap, Unwind}; pub use traphandlers::{call_wasm, LookupCodeSegment, RecordTrap, Unwind};
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]

View File

@@ -1,108 +1,17 @@
use errno; //! Memory management for linear memory.
use libc;
use mmap::Mmap;
use region; use region;
use std::fmt; use std::fmt;
use std::mem; use std::string::String;
use std::ptr;
use std::slice;
use wasmtime_environ::{MemoryPlan, MemoryStyle, WASM_MAX_PAGES, WASM_PAGE_SIZE}; 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<Self, String> {
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<Self, String> {
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. /// A linear memory instance.
/// ///
/// This linear memory has a stable base address and at the same time allows /// This linear memory has a stable base address and at the same time allows
/// for dynamical growing. /// for dynamical growing.
pub struct LinearMemory { pub struct LinearMemory {
ptrlen: PtrLen, mmap: Mmap,
current: u32, current: u32,
maximum: Option<u32>, maximum: Option<u32>,
offset_guard_size: usize, offset_guard_size: usize,
@@ -132,19 +41,19 @@ impl LinearMemory {
let unmapped_bytes = unmapped_pages * WASM_PAGE_SIZE as usize; let unmapped_bytes = unmapped_pages * WASM_PAGE_SIZE as usize;
let inaccessible_bytes = unmapped_bytes + offset_guard_bytes; 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. // Make the unmapped and offset-guard pages inaccessible.
unsafe { unsafe {
region::protect( region::protect(
ptrlen.ptr.add(mapped_bytes), mmap.as_ptr().add(mapped_bytes),
inaccessible_bytes, inaccessible_bytes,
region::Protection::Read, region::Protection::None,
).expect("unable to make memory readonly"); ).expect("unable to make memory inaccessible");
} }
Ok(Self { Ok(Self {
ptrlen, mmap,
current: plan.memory.minimum, current: plan.memory.minimum,
maximum: plan.memory.maximum, maximum: plan.memory.maximum,
offset_guard_size: offset_guard_bytes, offset_guard_size: offset_guard_bytes,
@@ -153,13 +62,13 @@ impl LinearMemory {
/// Returns an base address of this linear memory. /// Returns an base address of this linear memory.
pub fn base_addr(&mut self) -> *mut u8 { pub fn base_addr(&mut self) -> *mut u8 {
self.ptrlen.ptr self.mmap.as_mut_ptr()
} }
/// Returns a number of allocated wasm pages. /// Returns a number of allocated wasm pages.
pub fn current_size(&self) -> u32 { pub fn current_size(&self) -> u32 {
assert_eq!(self.ptrlen.len % WASM_PAGE_SIZE as usize, 0); assert_eq!(self.mmap.len() % WASM_PAGE_SIZE as usize, 0);
let num_pages = self.ptrlen.len / WASM_PAGE_SIZE as usize; let num_pages = self.mmap.len() / WASM_PAGE_SIZE as usize;
assert_eq!(num_pages as u32 as usize, num_pages); assert_eq!(num_pages as u32 as usize, num_pages);
num_pages as u32 num_pages as u32
} }
@@ -193,29 +102,29 @@ impl LinearMemory {
let new_bytes = new_pages as usize * WASM_PAGE_SIZE as usize; 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. // If we have no maximum, this is a "dynamic" heap, and it's allowed to move.
assert!(self.maximum.is_none()); assert!(self.maximum.is_none());
let mapped_pages = self.current as usize; let mapped_pages = self.current as usize;
let mapped_bytes = mapped_pages * WASM_PAGE_SIZE as usize; let mapped_bytes = mapped_pages * WASM_PAGE_SIZE as usize;
let guard_bytes = self.offset_guard_size; 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. // Make the offset-guard pages inaccessible.
unsafe { unsafe {
region::protect( region::protect(
new_ptrlen.ptr.add(mapped_bytes), new_mmap.as_ptr().add(mapped_bytes),
guard_bytes, guard_bytes,
region::Protection::Read, region::Protection::Read,
).expect("unable to make memory readonly"); ).expect("unable to make memory readonly");
} }
new_ptrlen new_mmap
.as_mut_slice() .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; self.current = new_pages;
@@ -235,25 +144,12 @@ impl fmt::Debug for LinearMemory {
impl AsRef<[u8]> for LinearMemory { impl AsRef<[u8]> for LinearMemory {
fn as_ref(&self) -> &[u8] { fn as_ref(&self) -> &[u8] {
self.ptrlen.as_slice() self.mmap.as_slice()
} }
} }
impl AsMut<[u8]> for LinearMemory { impl AsMut<[u8]> for LinearMemory {
fn as_mut(&mut self) -> &mut [u8] { fn as_mut(&mut self) -> &mut [u8] {
self.ptrlen.as_mut_slice() self.mmap.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);
} }
} }

136
lib/execute/src/mmap.rs Normal file
View File

@@ -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<Self, String> {
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<Self, String> {
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);
}
}

View File

@@ -6,6 +6,7 @@ use signalhandlers::{jmp_buf, CodeSegment};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::mem; use std::mem;
use std::ptr; use std::ptr;
use std::string::String;
// Currently we uset setjmp/longjmp to unwind out of a signal handler // Currently we uset setjmp/longjmp to unwind out of a signal handler
// and back to the point where WebAssembly was called (via `call_wasm`). // and back to the point where WebAssembly was called (via `call_wasm`).

View File

@@ -59,9 +59,9 @@ use std::io::prelude::*;
use std::io::stdout; use std::io::stdout;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{exit, Command}; use std::process::exit;
use wasmtime_environ::{Module, ModuleEnvironment, Tunables}; 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."; 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 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) { let instance = match compile_and_link_module(isa, &translation, &imports_resolver) {
Ok(compilation) => { Ok(compilation) => {
let mut instance = Instance::new( let mut instance = Instance::new(
@@ -165,11 +167,24 @@ fn handle_module(args: &Args, path: PathBuf, isa: &TargetIsa) -> Result<(), Stri
&translation.lazy.data_initializers, &translation.lazy.data_initializers,
)?; )?;
let mut context = let mut context = finish_instantiation(
finish_instantiation(&translation.module, &compilation, &mut instance)?; &mut code,
isa,
&translation.module,
&compilation,
&mut instance,
)?;
if let Some(ref f) = args.flag_function { 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 instance

297
tests/wast.rs Normal file
View File

@@ -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<Self, String> {
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<InvokeOutcome, String> {
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, String> {
InstanceWorld::new(code, isa, data)
}
struct Instances {
current: Option<InstanceWorld>,
namespace: HashMap<String, InstanceWorld>,
}
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<Vec<u8>, io::Error> {
let mut buf: Vec<u8> = 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);
}
}
}
}

View File

@@ -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")

View File

@@ -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")