Extract validation into a separate crate (#176)

* Add some docs.

* return_type isn't failable

* Add comment about safety of top_label

* Attempt number 10

* Rework.

Now we will a compiler which wraps and uses info from a evaluation simulator.

* Get rid of outcome

* Introduce StartedWith

* Actually use started_with.

* Mirror label_stack.

* Avoid using frame_type.

* Finally get rid from frame_type.

* Extract compilation

* Refactoring cleaning

* Validation separated from compilation.

* Move sink to FunctionReader

* Rename to compiler.

* fmt

* Move push_label under validation context.

* Add Validation traits

* Express the compiler using validation trait

* Move code under prepare

* Comments.

* WIP

* The great move of validation

* Make validation compile

* Make it compile.

* Format it.

* Fix warnings.

* Clean.

* Make it work under no_std

* Move deny_floating_point to wasmi

* Rename validate_module2 → validate_module

* Make validation tests work

* Make wasmi compilation tests work

* Renamings.

* Get rid of memory_units dependency in validation

* Rename.

* Clean.

* Estimate capacity.

* fmt.

* Clean and detail End opcode.

* Add comment about top_label safety

* Remove another TODO

* Comment access to require_target

* Remove redundant PartialEq

* Print value that can't be coerced to u32

* s/with_instruction_capacity/with_capacity

* fmt.

* fmt

* Proofs

* Add better proof

* Get rid of unreachable in StackValueType

* Propagate error if frame stack overflown on create

* use checked sub instead of -

* Keep::count
This commit is contained in:
Sergei Pepyakin 2019-04-19 16:05:09 +02:00 committed by GitHub
parent 0267b20e6e
commit a3aad8a549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3237 additions and 2445 deletions

View File

@ -10,15 +10,8 @@ description = "WebAssembly interpreter"
keywords = ["wasm", "webassembly", "bytecode", "interpreter"] keywords = ["wasm", "webassembly", "bytecode", "interpreter"]
exclude = [ "/res/*", "/tests/*", "/fuzz/*", "/benches/*" ] exclude = [ "/res/*", "/tests/*", "/fuzz/*", "/benches/*" ]
[features]
default = ["std"]
# Disable for no_std support
std = ["parity-wasm/std"]
# Enable for no_std support
# hashbrown only works on no_std
core = ["hashbrown/nightly", "libm"]
[dependencies] [dependencies]
wasmi-validation = { path = "validation", default-features = false }
parity-wasm = { version = "0.31", default-features = false } parity-wasm = { version = "0.31", default-features = false }
hashbrown = { version = "0.1.8", optional = true } hashbrown = { version = "0.1.8", optional = true }
memory_units = "0.3.0" memory_units = "0.3.0"
@ -28,3 +21,21 @@ libm = { version = "0.1.2", optional = true }
assert_matches = "1.1" assert_matches = "1.1"
rand = "0.4.2" rand = "0.4.2"
wabt = "0.6" wabt = "0.6"
[features]
default = ["std"]
# Disable for no_std support
std = [
"parity-wasm/std",
"wasmi-validation/std",
]
# Enable for no_std support
# hashbrown only works on no_std
core = [
"wasmi-validation/core",
"hashbrown/nightly",
"libm"
]
[workspace]
members = ["validation"]

View File

@ -1,8 +0,0 @@
pub mod stack;
/// Index of default linear memory.
pub const DEFAULT_MEMORY_INDEX: u32 = 0;
/// Index of default table.
pub const DEFAULT_TABLE_INDEX: u32 = 0;
// TODO: Move BlockFrame under validation.

View File

@ -1,5 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use alloc::rc::{Rc, Weak}; use alloc::rc::{Rc, Weak};
use core::fmt; use core::fmt;
use host::Externals; use host::Externals;

View File

@ -1,5 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use hashbrown::HashMap; use hashbrown::HashMap;

View File

@ -68,7 +68,7 @@
//! //!
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
/// Should we keep a value before "discarding" a stack frame? /// Should we keep a value before "discarding" a stack frame?
/// ///
@ -82,6 +82,16 @@ pub enum Keep {
Single, Single,
} }
impl Keep {
/// Reutrns a number of items that should be kept on the stack.
pub fn count(&self) -> u32 {
match *self {
Keep::None => 0,
Keep::Single => 1,
}
}
}
/// Specifies how many values we should keep and how many we should drop. /// Specifies how many values we should keep and how many we should drop.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct DropKeep { pub struct DropKeep {

View File

@ -97,7 +97,7 @@
#![warn(missing_docs)] #![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
//// alloc is required in no_std //// alloc is required in no_std
#![cfg_attr(not(feature = "std"), feature(alloc))] #![cfg_attr(not(feature = "std"), feature(alloc, alloc_prelude))]
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
#[macro_use] #[macro_use]
@ -110,18 +110,19 @@ extern crate std as alloc;
extern crate core; extern crate core;
#[cfg(test)] #[cfg(test)]
extern crate wabt;
#[cfg(test)]
#[macro_use]
extern crate assert_matches; extern crate assert_matches;
#[cfg(test)]
extern crate wabt;
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
extern crate hashbrown; extern crate hashbrown;
extern crate memory_units as memory_units_crate; extern crate memory_units as memory_units_crate;
extern crate parity_wasm; extern crate parity_wasm;
extern crate wasmi_validation as validation;
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use core::fmt; use core::fmt;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::error; use std::error;
@ -380,7 +381,6 @@ impl From<validation::Error> for Error {
} }
} }
mod common;
mod func; mod func;
mod global; mod global;
mod host; mod host;
@ -389,10 +389,10 @@ mod isa;
mod memory; mod memory;
mod module; mod module;
pub mod nan_preserving_float; pub mod nan_preserving_float;
mod prepare;
mod runner; mod runner;
mod table; mod table;
mod types; mod types;
mod validation;
mod value; mod value;
#[cfg(test)] #[cfg(test)]
@ -454,8 +454,7 @@ impl Module {
/// } /// }
/// ``` /// ```
pub fn from_parity_wasm_module(module: parity_wasm::elements::Module) -> Result<Module, Error> { pub fn from_parity_wasm_module(module: parity_wasm::elements::Module) -> Result<Module, Error> {
use validation::{validate_module, ValidatedModule}; let prepare::CompiledModule { code_map, module } = prepare::compile_module(module)?;
let ValidatedModule { code_map, module } = validate_module(module)?;
Ok(Module { code_map, module }) Ok(Module { code_map, module })
} }
@ -517,7 +516,7 @@ impl Module {
/// assert!(module.deny_floating_point().is_err()); /// assert!(module.deny_floating_point().is_err());
/// ``` /// ```
pub fn deny_floating_point(&self) -> Result<(), Error> { pub fn deny_floating_point(&self) -> Result<(), Error> {
validation::deny_floating_point(&self.module).map_err(Into::into) prepare::deny_floating_point(&self.module).map_err(Into::into)
} }
/// Create `Module` from a given buffer. /// Create `Module` from a given buffer.

View File

@ -1,11 +1,12 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use alloc::rc::Rc; use alloc::rc::Rc;
use core::cell::{Cell, RefCell}; use core::{
use core::cmp; cell::{Cell, RefCell},
use core::fmt; cmp, fmt,
use core::ops::Range; ops::Range,
use core::u32; u32,
};
use memory_units::{Bytes, Pages, RoundUpTo}; use memory_units::{Bytes, Pages, RoundUpTo};
use parity_wasm::elements::ResizableLimits; use parity_wasm::elements::ResizableLimits;
use value::LittleEndianConvert; use value::LittleEndianConvert;
@ -18,9 +19,6 @@ use Error;
/// [`MemoryInstance`]: struct.MemoryInstance.html /// [`MemoryInstance`]: struct.MemoryInstance.html
pub const LINEAR_MEMORY_PAGE_SIZE: Bytes = Bytes(65536); pub const LINEAR_MEMORY_PAGE_SIZE: Bytes = Bytes(65536);
/// Maximal number of pages.
const LINEAR_MEMORY_MAX_PAGES: Pages = Pages(65536);
/// Reference to a memory (See [`MemoryInstance`] for details). /// Reference to a memory (See [`MemoryInstance`] for details).
/// ///
/// This reference has a reference-counting semantics. /// This reference has a reference-counting semantics.
@ -111,7 +109,22 @@ impl MemoryInstance {
/// ///
/// [`LINEAR_MEMORY_PAGE_SIZE`]: constant.LINEAR_MEMORY_PAGE_SIZE.html /// [`LINEAR_MEMORY_PAGE_SIZE`]: constant.LINEAR_MEMORY_PAGE_SIZE.html
pub fn alloc(initial: Pages, maximum: Option<Pages>) -> Result<MemoryRef, Error> { pub fn alloc(initial: Pages, maximum: Option<Pages>) -> Result<MemoryRef, Error> {
validate_memory(initial, maximum).map_err(Error::Memory)?; {
use std::convert::TryInto;
let initial_u32: u32 = initial.0.try_into().map_err(|_| {
Error::Memory(format!("initial ({}) can't be coerced to u32", initial.0))
})?;
let maximum_u32: Option<u32> = match maximum {
Some(maximum_pages) => Some(maximum_pages.0.try_into().map_err(|_| {
Error::Memory(format!(
"maximum ({}) can't be coerced to u32",
maximum_pages.0
))
})?),
None => None,
};
validation::validate_memory(initial_u32, maximum_u32).map_err(Error::Memory)?;
}
let memory = MemoryInstance::new(initial, maximum); let memory = MemoryInstance::new(initial, maximum);
Ok(MemoryRef(Rc::new(memory))) Ok(MemoryRef(Rc::new(memory)))
@ -271,7 +284,9 @@ impl MemoryInstance {
} }
let new_size: Pages = size_before_grow + additional; let new_size: Pages = size_before_grow + additional;
let maximum = self.maximum.unwrap_or(LINEAR_MEMORY_MAX_PAGES); let maximum = self
.maximum
.unwrap_or(Pages(validation::LINEAR_MEMORY_MAX_PAGES as usize));
if new_size > maximum { if new_size > maximum {
return Err(Error::Memory(format!( return Err(Error::Memory(format!(
"Trying to grow memory by {} pages when already have {}", "Trying to grow memory by {} pages when already have {}",
@ -549,31 +564,6 @@ impl MemoryInstance {
} }
} }
pub fn validate_memory(initial: Pages, maximum: Option<Pages>) -> Result<(), String> {
if initial > LINEAR_MEMORY_MAX_PAGES {
return Err(format!(
"initial memory size must be at most {} pages",
LINEAR_MEMORY_MAX_PAGES.0
));
}
if let Some(maximum) = maximum {
if initial > maximum {
return Err(format!(
"maximum limit {} is less than minimum {}",
maximum.0, initial.0,
));
}
if maximum > LINEAR_MEMORY_MAX_PAGES {
return Err(format!(
"maximum memory size must be at most {} pages",
LINEAR_MEMORY_MAX_PAGES.0
));
}
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -1,5 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use alloc::rc::Rc; use alloc::rc::Rc;
use core::cell::RefCell; use core::cell::RefCell;
use core::fmt; use core::fmt;
@ -10,7 +10,6 @@ use hashbrown::HashMap;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::collections::HashMap; use std::collections::HashMap;
use common::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX};
use core::cell::Ref; use core::cell::Ref;
use func::{FuncBody, FuncInstance, FuncRef}; use func::{FuncBody, FuncInstance, FuncRef};
use global::{GlobalInstance, GlobalRef}; use global::{GlobalInstance, GlobalRef};
@ -21,6 +20,7 @@ use memory_units::Pages;
use parity_wasm::elements::{External, InitExpr, Instruction, Internal, ResizableLimits, Type}; use parity_wasm::elements::{External, InitExpr, Instruction, Internal, ResizableLimits, Type};
use table::TableRef; use table::TableRef;
use types::{GlobalDescriptor, MemoryDescriptor, TableDescriptor}; use types::{GlobalDescriptor, MemoryDescriptor, TableDescriptor};
use validation::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX};
use {Error, MemoryInstance, Module, RuntimeValue, Signature, TableInstance}; use {Error, MemoryInstance, Module, RuntimeValue, Signature, TableInstance};
/// Reference to a [`ModuleInstance`]. /// Reference to a [`ModuleInstance`].

View File

@ -153,9 +153,11 @@ mod tests {
use super::{F32, F64}; use super::{F32, F64};
use core::fmt::Debug; use core::{
use core::iter; fmt::Debug,
use core::ops::{Add, Div, Mul, Neg, Sub}; iter,
ops::{Add, Div, Mul, Neg, Sub},
};
fn test_ops<T, F, I>(iter: I) fn test_ops<T, F, I>(iter: I)
where where

1276
src/prepare/compile.rs Normal file

File diff suppressed because it is too large Load Diff

170
src/prepare/mod.rs Normal file
View File

@ -0,0 +1,170 @@
#[allow(unused_imports)]
use alloc::prelude::v1::*;
use crate::{
isa,
validation::{validate_module, Error, Validator},
};
use parity_wasm::elements::Module;
mod compile;
#[cfg(test)]
mod tests;
#[derive(Clone)]
pub struct CompiledModule {
pub code_map: Vec<isa::Instructions>,
pub module: Module,
}
pub struct WasmiValidation {
code_map: Vec<isa::Instructions>,
}
// This implementation of `Validation` is compiling wasm code at the
// validation time.
impl Validator for WasmiValidation {
type Output = Vec<isa::Instructions>;
type FuncValidator = compile::Compiler;
fn new(_module: &Module) -> Self {
WasmiValidation {
// TODO: with capacity?
code_map: Vec::new(),
}
}
fn on_function_validated(&mut self, _index: u32, output: isa::Instructions) {
self.code_map.push(output);
}
fn finish(self) -> Vec<isa::Instructions> {
self.code_map
}
}
/// Validate a module and compile it to the internal representation.
pub fn compile_module(module: Module) -> Result<CompiledModule, Error> {
let code_map = validate_module::<WasmiValidation>(&module)?;
Ok(CompiledModule { module, code_map })
}
/// Verify that the module doesn't use floating point instructions or types.
///
/// Returns `Err` if
///
/// - Any of function bodies uses a floating pointer instruction (an instruction that
/// consumes or produces a value of a floating point type)
/// - If a floating point type used in a definition of a function.
pub fn deny_floating_point(module: &Module) -> Result<(), Error> {
use parity_wasm::elements::{
Instruction::{self, *},
Type, ValueType,
};
if let Some(code) = module.code_section() {
for op in code.bodies().iter().flat_map(|body| body.code().elements()) {
macro_rules! match_eq {
($pattern:pat) => {
|val| if let $pattern = *val { true } else { false }
};
}
const DENIED: &[fn(&Instruction) -> bool] = &[
match_eq!(F32Load(_, _)),
match_eq!(F64Load(_, _)),
match_eq!(F32Store(_, _)),
match_eq!(F64Store(_, _)),
match_eq!(F32Const(_)),
match_eq!(F64Const(_)),
match_eq!(F32Eq),
match_eq!(F32Ne),
match_eq!(F32Lt),
match_eq!(F32Gt),
match_eq!(F32Le),
match_eq!(F32Ge),
match_eq!(F64Eq),
match_eq!(F64Ne),
match_eq!(F64Lt),
match_eq!(F64Gt),
match_eq!(F64Le),
match_eq!(F64Ge),
match_eq!(F32Abs),
match_eq!(F32Neg),
match_eq!(F32Ceil),
match_eq!(F32Floor),
match_eq!(F32Trunc),
match_eq!(F32Nearest),
match_eq!(F32Sqrt),
match_eq!(F32Add),
match_eq!(F32Sub),
match_eq!(F32Mul),
match_eq!(F32Div),
match_eq!(F32Min),
match_eq!(F32Max),
match_eq!(F32Copysign),
match_eq!(F64Abs),
match_eq!(F64Neg),
match_eq!(F64Ceil),
match_eq!(F64Floor),
match_eq!(F64Trunc),
match_eq!(F64Nearest),
match_eq!(F64Sqrt),
match_eq!(F64Add),
match_eq!(F64Sub),
match_eq!(F64Mul),
match_eq!(F64Div),
match_eq!(F64Min),
match_eq!(F64Max),
match_eq!(F64Copysign),
match_eq!(F32ConvertSI32),
match_eq!(F32ConvertUI32),
match_eq!(F32ConvertSI64),
match_eq!(F32ConvertUI64),
match_eq!(F32DemoteF64),
match_eq!(F64ConvertSI32),
match_eq!(F64ConvertUI32),
match_eq!(F64ConvertSI64),
match_eq!(F64ConvertUI64),
match_eq!(F64PromoteF32),
match_eq!(F32ReinterpretI32),
match_eq!(F64ReinterpretI64),
match_eq!(I32TruncSF32),
match_eq!(I32TruncUF32),
match_eq!(I32TruncSF64),
match_eq!(I32TruncUF64),
match_eq!(I64TruncSF32),
match_eq!(I64TruncUF32),
match_eq!(I64TruncSF64),
match_eq!(I64TruncUF64),
match_eq!(I32ReinterpretF32),
match_eq!(I64ReinterpretF64),
];
if DENIED.iter().any(|is_denied| is_denied(op)) {
return Err(Error(format!("Floating point operation denied: {:?}", op)));
}
}
}
if let (Some(sec), Some(types)) = (module.function_section(), module.type_section()) {
let types = types.types();
for sig in sec.entries() {
if let Some(typ) = types.get(sig.type_ref() as usize) {
match *typ {
Type::Function(ref func) => {
if func
.params()
.iter()
.chain(func.return_type().as_ref())
.any(|&typ| typ == ValueType::F32 || typ == ValueType::F64)
{
return Err(Error(format!("Use of floating point types denied")));
}
}
}
}
}
}
Ok(())
}

View File

@ -1,285 +1,17 @@
use super::{validate_module, ValidatedModule}; use super::{compile_module, CompiledModule};
use parity_wasm::{deserialize_buffer, elements::Module};
use isa; use isa;
use parity_wasm::builder::module;
use parity_wasm::elements::{
deserialize_buffer, BlockType, External, GlobalEntry, GlobalType, ImportEntry, InitExpr,
Instruction, Instructions, MemoryType, Module, TableType, ValueType,
};
use wabt; use wabt;
#[test] fn validate(wat: &str) -> CompiledModule {
fn empty_is_valid() {
let module = module().build();
assert!(validate_module(module).is_ok());
}
#[test]
fn limits() {
let test_cases = vec![
// min > max
(10, Some(9), false),
// min = max
(10, Some(10), true),
// table/memory is always valid without max
(10, None, true),
];
for (min, max, is_valid) in test_cases {
// defined table
let m = module().table().with_min(min).with_max(max).build().build();
assert_eq!(validate_module(m).is_ok(), is_valid);
// imported table
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"table".into(),
External::Table(TableType::new(min, max)),
))
.build();
assert_eq!(validate_module(m).is_ok(), is_valid);
// defined memory
let m = module()
.memory()
.with_min(min)
.with_max(max)
.build()
.build();
assert_eq!(validate_module(m).is_ok(), is_valid);
// imported table
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"memory".into(),
External::Memory(MemoryType::new(min, max)),
))
.build();
assert_eq!(validate_module(m).is_ok(), is_valid);
}
}
#[test]
fn global_init_const() {
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::I32Const(42), Instruction::End]),
))
.build();
assert!(validate_module(m).is_ok());
// init expr type differs from declared global type
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I64, true),
InitExpr::new(vec![Instruction::I32Const(42), Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
}
#[test]
fn global_init_global() {
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, false)),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(m).is_ok());
// get_global can reference only previously defined globals
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
// get_global can reference only const globals
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, true)),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
// get_global in init_expr can only refer to imported globals.
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, false),
InitExpr::new(vec![Instruction::I32Const(0), Instruction::End]),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
}
#[test]
fn global_init_misc() {
// without delimiting End opcode
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::I32Const(42)]),
))
.build();
assert!(validate_module(m).is_err());
// empty init expr
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
// not an constant opcode used
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::Unreachable, Instruction::End]),
))
.build();
assert!(validate_module(m).is_err());
}
#[test]
fn module_limits_validity() {
// module cannot contain more than 1 memory atm.
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"memory".into(),
External::Memory(MemoryType::new(10, None)),
))
.memory()
.with_min(10)
.build()
.build();
assert!(validate_module(m).is_err());
// module cannot contain more than 1 table atm.
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"table".into(),
External::Table(TableType::new(10, None)),
))
.table()
.with_min(10)
.build()
.build();
assert!(validate_module(m).is_err());
}
#[test]
fn funcs() {
// recursive function calls is legal.
let m = module()
.function()
.signature()
.return_type()
.i32()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::Call(1),
Instruction::End,
]))
.build()
.build()
.function()
.signature()
.return_type()
.i32()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::Call(0),
Instruction::End,
]))
.build()
.build()
.build();
assert!(validate_module(m).is_ok());
}
#[test]
fn globals() {
// import immutable global is legal.
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, false)),
))
.build();
assert!(validate_module(m).is_ok());
// import mutable global is invalid.
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, true)),
))
.build();
assert!(validate_module(m).is_err());
}
#[test]
fn if_else_with_return_type_validation() {
let m = module()
.function()
.signature()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::I32Const(1),
Instruction::If(BlockType::NoResult),
Instruction::I32Const(1),
Instruction::If(BlockType::Value(ValueType::I32)),
Instruction::I32Const(1),
Instruction::Else,
Instruction::I32Const(2),
Instruction::End,
Instruction::Drop,
Instruction::End,
Instruction::End,
]))
.build()
.build()
.build();
validate_module(m).unwrap();
}
fn validate(wat: &str) -> ValidatedModule {
let wasm = wabt::wat2wasm(wat).unwrap(); let wasm = wabt::wat2wasm(wat).unwrap();
let module = deserialize_buffer::<Module>(&wasm).unwrap(); let module = deserialize_buffer::<Module>(&wasm).unwrap();
let validated_module = validate_module(module).unwrap(); let compiled_module = compile_module(module).unwrap();
validated_module compiled_module
} }
fn compile(module: &ValidatedModule) -> (Vec<isa::Instruction>, Vec<u32>) { fn compile(module: &CompiledModule) -> (Vec<isa::Instruction>, Vec<u32>) {
let code = &module.code_map[0]; let code = &module.code_map[0];
let mut instructions = Vec::new(); let mut instructions = Vec::new();
let mut pcs = Vec::new(); let mut pcs = Vec::new();
@ -806,6 +538,68 @@ fn loop_empty() {
) )
} }
#[test]
fn spec_as_br_if_value_cond() {
use self::isa::Instruction::*;
let module = validate(
r#"
(func (export "as-br_if-value-cond") (result i32)
(block (result i32)
(drop
(br_if 0
(i32.const 6)
(br_table 0 0
(i32.const 9)
(i32.const 0)
)
)
)
(i32.const 7)
)
)
"#,
);
let (code, _) = compile(&module);
assert_eq!(
code,
vec![
I32Const(6),
I32Const(9),
I32Const(0),
isa::Instruction::BrTable(targets![
isa::Target {
dst_pc: 9,
drop_keep: isa::DropKeep {
drop: 1,
keep: isa::Keep::Single
}
},
isa::Target {
dst_pc: 9,
drop_keep: isa::DropKeep {
drop: 1,
keep: isa::Keep::Single
}
}
]),
BrIfNez(isa::Target {
dst_pc: 9,
drop_keep: isa::DropKeep {
drop: 0,
keep: isa::Keep::Single
}
}),
Drop,
I32Const(7),
Return(isa::DropKeep {
drop: 0,
keep: isa::Keep::Single
})
]
);
}
#[test] #[test]
fn brtable() { fn brtable() {
let module = validate( let module = validate(

View File

@ -1,6 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use common::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX};
use core::fmt; use core::fmt;
use core::ops; use core::ops;
use core::{u32, usize}; use core::{u32, usize};
@ -12,6 +11,7 @@ use memory_units::Pages;
use module::ModuleRef; use module::ModuleRef;
use nan_preserving_float::{F32, F64}; use nan_preserving_float::{F32, F64};
use parity_wasm::elements::Local; use parity_wasm::elements::Local;
use validation::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX};
use value::{ use value::{
ArithmeticOps, ExtendInto, Float, Integer, LittleEndianConvert, RuntimeValue, TransmuteInto, ArithmeticOps, ExtendInto, Float, Integer, LittleEndianConvert, RuntimeValue, TransmuteInto,
TryTruncateInto, WrapInto, TryTruncateInto, WrapInto,

View File

@ -1,5 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use alloc::rc::Rc; use alloc::rc::Rc;
use core::cell::RefCell; use core::cell::RefCell;
use core::fmt; use core::fmt;

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,6 @@ set -eux
cd $(dirname $0) cd $(dirname $0)
time cargo test time cargo test --all
cd - cd -

19
validation/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "wasmi-validation"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
parity-wasm = { version = "0.31", default-features = false }
hashbrown = { version = "0.1.8", optional = true }
[dev-dependencies]
assert_matches = "1.1"
[features]
default = ["std"]
std = ["parity-wasm/std"]
core = [
"hashbrown/nightly"
]

View File

@ -1,9 +1,9 @@
use crate::Error;
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use parity_wasm::elements::{ use parity_wasm::elements::{
BlockType, FunctionType, GlobalType, MemoryType, TableType, ValueType, BlockType, FunctionType, GlobalType, MemoryType, TableType, ValueType,
}; };
use validation::Error;
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ModuleContext { pub struct ModuleContext {

1219
validation/src/func.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,28 @@
// TODO: Uncomment
// #![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
//// alloc is required in no_std
#![cfg_attr(not(feature = "std"), feature(alloc, alloc_prelude))]
#[cfg(not(feature = "std"))]
#[macro_use]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std as alloc;
pub mod stack;
/// Index of default linear memory.
pub const DEFAULT_MEMORY_INDEX: u32 = 0;
/// Index of default table.
pub const DEFAULT_TABLE_INDEX: u32 = 0;
/// Maximal number of pages that a wasm instance supports.
pub const LINEAR_MEMORY_MAX_PAGES: u32 = 65536;
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use core::fmt; use core::fmt;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::error; use std::error;
@ -10,24 +33,22 @@ use hashbrown::HashSet;
use std::collections::HashSet; use std::collections::HashSet;
use self::context::ModuleContextBuilder; use self::context::ModuleContextBuilder;
use self::func::FunctionReader;
use common::stack;
use isa;
use memory_units::Pages;
use parity_wasm::elements::{ use parity_wasm::elements::{
BlockType, External, GlobalEntry, GlobalType, InitExpr, Instruction, Internal, MemoryType, BlockType, External, FuncBody, GlobalEntry, GlobalType, InitExpr, Instruction, Internal,
Module, ResizableLimits, TableType, Type, ValueType, MemoryType, Module, ResizableLimits, TableType, Type, ValueType,
}; };
mod context; pub mod context;
mod func; pub mod func;
mod util; pub mod util;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
// TODO: Consider using a type other than String, because
// of formatting machinary is not welcomed in substrate runtimes.
#[derive(Debug)] #[derive(Debug)]
pub struct Error(String); pub struct Error(pub String);
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -48,137 +69,77 @@ impl From<stack::Error> for Error {
} }
} }
#[derive(Clone)] pub trait Validator {
pub struct ValidatedModule { type Output;
pub code_map: Vec<isa::Instructions>, type FuncValidator: FuncValidator;
pub module: Module, fn new(module: &Module) -> Self;
fn on_function_validated(
&mut self,
index: u32,
output: <<Self as Validator>::FuncValidator as FuncValidator>::Output,
);
fn finish(self) -> Self::Output;
} }
impl ::core::ops::Deref for ValidatedModule { pub trait FuncValidator {
type Target = Module; type Output;
fn deref(&self) -> &Module { fn new(ctx: &func::FunctionValidationContext, body: &FuncBody) -> Self;
&self.module fn next_instruction(
&mut self,
ctx: &mut func::FunctionValidationContext,
instruction: &Instruction,
) -> Result<(), Error>;
fn finish(self) -> Self::Output;
}
/// A module validator that just validates modules and produces no result.
pub struct PlainValidator;
impl Validator for PlainValidator {
type Output = ();
type FuncValidator = PlainFuncValidator;
fn new(_module: &Module) -> PlainValidator {
PlainValidator
}
fn on_function_validated(
&mut self,
_index: u32,
_output: <<Self as Validator>::FuncValidator as FuncValidator>::Output,
) -> () {
()
}
fn finish(self) -> () {
()
} }
} }
pub fn deny_floating_point(module: &Module) -> Result<(), Error> { /// A function validator that just validates modules and produces no result.
if let Some(code) = module.code_section() { pub struct PlainFuncValidator;
for op in code.bodies().iter().flat_map(|body| body.code().elements()) {
use parity_wasm::elements::Instruction::*;
macro_rules! match_eq { impl FuncValidator for PlainFuncValidator {
($pattern:pat) => { type Output = ();
|val| if let $pattern = *val { true } else { false }
};
}
const DENIED: &[fn(&Instruction) -> bool] = &[ fn new(_ctx: &func::FunctionValidationContext, _body: &FuncBody) -> PlainFuncValidator {
match_eq!(F32Load(_, _)), PlainFuncValidator
match_eq!(F64Load(_, _)),
match_eq!(F32Store(_, _)),
match_eq!(F64Store(_, _)),
match_eq!(F32Const(_)),
match_eq!(F64Const(_)),
match_eq!(F32Eq),
match_eq!(F32Ne),
match_eq!(F32Lt),
match_eq!(F32Gt),
match_eq!(F32Le),
match_eq!(F32Ge),
match_eq!(F64Eq),
match_eq!(F64Ne),
match_eq!(F64Lt),
match_eq!(F64Gt),
match_eq!(F64Le),
match_eq!(F64Ge),
match_eq!(F32Abs),
match_eq!(F32Neg),
match_eq!(F32Ceil),
match_eq!(F32Floor),
match_eq!(F32Trunc),
match_eq!(F32Nearest),
match_eq!(F32Sqrt),
match_eq!(F32Add),
match_eq!(F32Sub),
match_eq!(F32Mul),
match_eq!(F32Div),
match_eq!(F32Min),
match_eq!(F32Max),
match_eq!(F32Copysign),
match_eq!(F64Abs),
match_eq!(F64Neg),
match_eq!(F64Ceil),
match_eq!(F64Floor),
match_eq!(F64Trunc),
match_eq!(F64Nearest),
match_eq!(F64Sqrt),
match_eq!(F64Add),
match_eq!(F64Sub),
match_eq!(F64Mul),
match_eq!(F64Div),
match_eq!(F64Min),
match_eq!(F64Max),
match_eq!(F64Copysign),
match_eq!(F32ConvertSI32),
match_eq!(F32ConvertUI32),
match_eq!(F32ConvertSI64),
match_eq!(F32ConvertUI64),
match_eq!(F32DemoteF64),
match_eq!(F64ConvertSI32),
match_eq!(F64ConvertUI32),
match_eq!(F64ConvertSI64),
match_eq!(F64ConvertUI64),
match_eq!(F64PromoteF32),
match_eq!(F32ReinterpretI32),
match_eq!(F64ReinterpretI64),
match_eq!(I32TruncSF32),
match_eq!(I32TruncUF32),
match_eq!(I32TruncSF64),
match_eq!(I32TruncUF64),
match_eq!(I64TruncSF32),
match_eq!(I64TruncUF32),
match_eq!(I64TruncSF64),
match_eq!(I64TruncUF64),
match_eq!(I32ReinterpretF32),
match_eq!(I64ReinterpretF64),
];
if DENIED.iter().any(|is_denied| is_denied(op)) {
return Err(Error(format!("Floating point operation denied: {:?}", op)));
}
}
} }
if let (Some(sec), Some(types)) = (module.function_section(), module.type_section()) { fn next_instruction(
use parity_wasm::elements::{Type, ValueType}; &mut self,
ctx: &mut func::FunctionValidationContext,
let types = types.types(); instruction: &Instruction,
) -> Result<(), Error> {
for sig in sec.entries() { ctx.step(instruction)
if let Some(typ) = types.get(sig.type_ref() as usize) {
match *typ {
Type::Function(ref func) => {
if func
.params()
.iter()
.chain(func.return_type().as_ref())
.any(|&typ| typ == ValueType::F32 || typ == ValueType::F64)
{
return Err(Error(format!("Use of floating point types denied")));
}
}
}
}
}
} }
Ok(()) fn finish(self) -> () {
()
}
} }
pub fn validate_module(module: Module) -> Result<ValidatedModule, Error> { pub fn validate_module<V: Validator>(module: &Module) -> Result<V::Output, Error> {
let mut context_builder = ModuleContextBuilder::new(); let mut context_builder = ModuleContextBuilder::new();
let mut imported_globals = Vec::new(); let mut imported_globals = Vec::new();
let mut code_map = Vec::new(); let mut validation = V::new(&module);
// Copy types from module as is. // Copy types from module as is.
context_builder.set_types( context_builder.set_types(
@ -265,15 +226,15 @@ pub fn validate_module(module: Module) -> Result<ValidatedModule, Error> {
.bodies() .bodies()
.get(index as usize) .get(index as usize)
.ok_or(Error(format!("Missing body for function {}", index)))?; .ok_or(Error(format!("Missing body for function {}", index)))?;
let code =
FunctionReader::read_function(&context, function, function_body).map_err(|e| { let output = func::drive::<V::FuncValidator>(&context, function, function_body)
let Error(ref msg) = e; .map_err(|Error(ref msg)| {
Error(format!( Error(format!(
"Function #{} reading/validation error: {}", "Function #{} reading/validation error: {}",
index, msg index, msg
)) ))
})?; })?;
code_map.push(code); validation.on_function_validated(index as u32, output);
} }
} }
@ -381,7 +342,7 @@ pub fn validate_module(module: Module) -> Result<ValidatedModule, Error> {
} }
} }
Ok(ValidatedModule { module, code_map }) Ok(validation.finish())
} }
fn validate_limits(limits: &ResizableLimits) -> Result<(), Error> { fn validate_limits(limits: &ResizableLimits) -> Result<(), Error> {
@ -398,9 +359,34 @@ fn validate_limits(limits: &ResizableLimits) -> Result<(), Error> {
} }
fn validate_memory_type(memory_type: &MemoryType) -> Result<(), Error> { fn validate_memory_type(memory_type: &MemoryType) -> Result<(), Error> {
let initial: Pages = Pages(memory_type.limits().initial() as usize); let initial = memory_type.limits().initial();
let maximum: Option<Pages> = memory_type.limits().maximum().map(|m| Pages(m as usize)); let maximum: Option<u32> = memory_type.limits().maximum();
::memory::validate_memory(initial, maximum).map_err(Error) validate_memory(initial, maximum).map_err(Error)
}
pub fn validate_memory(initial: u32, maximum: Option<u32>) -> Result<(), String> {
if initial > LINEAR_MEMORY_MAX_PAGES {
return Err(format!(
"initial memory size must be at most {} pages",
LINEAR_MEMORY_MAX_PAGES
));
}
if let Some(maximum) = maximum {
if initial > maximum {
return Err(format!(
"maximum limit {} is less than minimum {}",
maximum, initial,
));
}
if maximum > LINEAR_MEMORY_MAX_PAGES {
return Err(format!(
"maximum memory size must be at most {} pages",
LINEAR_MEMORY_MAX_PAGES
));
}
}
Ok(())
} }
fn validate_table_type(table_type: &TableType) -> Result<(), Error> { fn validate_table_type(table_type: &TableType) -> Result<(), Error> {

View File

@ -1,5 +1,5 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use core::fmt; use core::fmt;
#[cfg(feature = "std")] #[cfg(feature = "std")]

277
validation/src/tests.rs Normal file
View File

@ -0,0 +1,277 @@
use crate::{Error, PlainValidator};
use parity_wasm::{
builder::module,
elements::{
BlockType, External, GlobalEntry, GlobalType, ImportEntry, InitExpr, Instruction,
Instructions, MemoryType, Module, TableType, ValueType,
},
};
fn validate_module(module: &Module) -> Result<(), Error> {
super::validate_module::<PlainValidator>(module)
}
#[test]
fn empty_is_valid() {
let module = module().build();
assert!(validate_module(&module).is_ok());
}
#[test]
fn limits() {
let test_cases = vec![
// min > max
(10, Some(9), false),
// min = max
(10, Some(10), true),
// table/memory is always valid without max
(10, None, true),
];
for (min, max, is_valid) in test_cases {
// defined table
let m = module().table().with_min(min).with_max(max).build().build();
assert_eq!(validate_module(&m).is_ok(), is_valid);
// imported table
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"table".into(),
External::Table(TableType::new(min, max)),
))
.build();
assert_eq!(validate_module(&m).is_ok(), is_valid);
// defined memory
let m = module()
.memory()
.with_min(min)
.with_max(max)
.build()
.build();
assert_eq!(validate_module(&m).is_ok(), is_valid);
// imported table
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"memory".into(),
External::Memory(MemoryType::new(min, max)),
))
.build();
assert_eq!(validate_module(&m).is_ok(), is_valid);
}
}
#[test]
fn global_init_const() {
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::I32Const(42), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_ok());
// init expr type differs from declared global type
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I64, true),
InitExpr::new(vec![Instruction::I32Const(42), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
}
#[test]
fn global_init_global() {
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, false)),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_ok());
// get_global can reference only previously defined globals
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
// get_global can reference only const globals
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, true)),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
// get_global in init_expr can only refer to imported globals.
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, false),
InitExpr::new(vec![Instruction::I32Const(0), Instruction::End]),
))
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::GetGlobal(0), Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
}
#[test]
fn global_init_misc() {
// without delimiting End opcode
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::I32Const(42)]),
))
.build();
assert!(validate_module(&m).is_err());
// empty init expr
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
// not an constant opcode used
let m = module()
.with_global(GlobalEntry::new(
GlobalType::new(ValueType::I32, true),
InitExpr::new(vec![Instruction::Unreachable, Instruction::End]),
))
.build();
assert!(validate_module(&m).is_err());
}
#[test]
fn module_limits_validity() {
// module cannot contain more than 1 memory atm.
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"memory".into(),
External::Memory(MemoryType::new(10, None)),
))
.memory()
.with_min(10)
.build()
.build();
assert!(validate_module(&m).is_err());
// module cannot contain more than 1 table atm.
let m = module()
.with_import(ImportEntry::new(
"core".into(),
"table".into(),
External::Table(TableType::new(10, None)),
))
.table()
.with_min(10)
.build()
.build();
assert!(validate_module(&m).is_err());
}
#[test]
fn funcs() {
// recursive function calls is legal.
let m = module()
.function()
.signature()
.return_type()
.i32()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::Call(1),
Instruction::End,
]))
.build()
.build()
.function()
.signature()
.return_type()
.i32()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::Call(0),
Instruction::End,
]))
.build()
.build()
.build();
assert!(validate_module(&m).is_ok());
}
#[test]
fn globals() {
// import immutable global is legal.
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, false)),
))
.build();
assert!(validate_module(&m).is_ok());
// import mutable global is invalid.
let m = module()
.with_import(ImportEntry::new(
"env".into(),
"ext_global".into(),
External::Global(GlobalType::new(ValueType::I32, true)),
))
.build();
assert!(validate_module(&m).is_err());
}
#[test]
fn if_else_with_return_type_validation() {
let m = module()
.function()
.signature()
.build()
.body()
.with_instructions(Instructions::new(vec![
Instruction::I32Const(1),
Instruction::If(BlockType::NoResult),
Instruction::I32Const(1),
Instruction::If(BlockType::Value(ValueType::I32)),
Instruction::I32Const(1),
Instruction::Else,
Instruction::I32Const(2),
Instruction::End,
Instruction::Drop,
Instruction::End,
Instruction::End,
]))
.build()
.build()
.build();
validate_module(&m).unwrap();
}

View File

@ -1,7 +1,10 @@
use crate::Error;
#[allow(unused_imports)] #[allow(unused_imports)]
use alloc::prelude::*; use alloc::prelude::v1::*;
use parity_wasm::elements::{Local, ValueType}; use parity_wasm::elements::{Local, ValueType};
use validation::Error;
#[cfg(test)]
use assert_matches::assert_matches;
/// Locals are the concatenation of a slice of function parameters /// Locals are the concatenation of a slice of function parameters
/// with function declared local variables. /// with function declared local variables.