typesafe api with tests for StackWithLimit

This commit is contained in:
Andrew Dirksen 2018-11-16 19:46:57 -08:00
parent 47dd23f601
commit 94b10306b2
3 changed files with 253 additions and 162 deletions

View File

@ -1,135 +1,20 @@
mod ol {
#[allow(unused_imports)]
use alloc::prelude::*;
use core::marker::PhantomData;
use core::usize;
use core::fmt;
#[cfg(feature = "std")]
use std::error;
#[derive(Debug)]
pub struct Error(String);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "std")]
impl error::Error for Error {
fn description(&self) -> &str {
&self.0
}
}
/// Stack with limit.
#[derive(Debug)]
pub struct StackWithLimit<T>
where
T: Clone,
{
/// Stack values.
values: Vec<T>,
/// Stack limit (maximal stack len).
limit: usize,
}
impl<T> StackWithLimit<T>
where
T: Clone,
{
pub fn with_limit(limit: usize) -> Self {
StackWithLimit {
values: Vec::new(),
limit: limit,
}
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn top(&self) -> Result<&T, Error> {
self.values
.last()
.ok_or_else(|| Error("non-empty stack expected".into()))
}
pub fn top_mut(&mut self) -> Result<&mut T, Error> {
self.values
.last_mut()
.ok_or_else(|| Error("non-empty stack expected".into()))
}
// Not the same as vector.get
pub fn get(&self, index: usize) -> Result<&T, Error> {
if index >= self.values.len() {
return Err(Error(format!(
"trying to get value at position {} on stack of size {}",
index,
self.values.len()
)));
}
Ok(self
.values
.get(self.values.len() - 1 - index)
.expect("checked couple of lines above"))
}
pub fn push(&mut self, value: T) -> Result<(), Error> {
if self.values.len() >= self.limit {
return Err(Error(format!("exceeded stack limit {}", self.limit)));
}
self.values.push(value);
Ok(())
}
pub fn pop(&mut self) -> Result<T, Error> {
self.values
.pop()
.ok_or_else(|| Error("non-empty stack expected".into()))
}
pub fn resize(&mut self, new_size: usize, dummy: T) {
debug_assert!(new_size <= self.values.len());
self.values.resize(new_size, dummy);
}
}
}
#[allow(unused_imports)]
use alloc::prelude::*;
#[derive(Copy, Clone, Debug)]
pub struct StackOverflow;
// impl From<StackOverflow> for TrapKind {
// fn from(_: StackOverflow) -> TrapKind {
// TrapKind::StackOverflow
// }
// }
// impl From<StackOverflow> for Trap {
// fn from(_: StackOverflow) -> Trap {
// Trap::new(TrapKind::StackOverflow)
// }
// }
// TODO: impl constructors
struct Limit(usize);
struct InitialSize(usize);
/// Pre-allocated, growable stack with upper bound on size.
///
/// StackWithLimit is guaranteed never to grow larger than a set limit.
/// When limit is reached attempting to push to the stack will return
/// `Err(StackOverflow)`.
///
/// In addition to the limit. Initial stack size is configurable.
/// `StackWithLimit` will start out with initial size, but grow if necessary.
/// Both limit and initial stack size are configurable.
/// `StackWithLimit` will start out with initial size, but grow when necessary.
#[derive(Debug)]
pub struct StackWithLimit<T> {
stack: Vec<T>,
@ -137,26 +22,54 @@ pub struct StackWithLimit<T> {
}
impl<T> StackWithLimit<T> {
/// Create an new StackWithLimit with `limit` max size and `initial_size` elements pre-allocated
/// `initial_size` should not be larger than `limit`
pub fn new(initial_size: usize, limit: usize) -> StackWithLimit<T> {
/// Create a StackWithLimit with `limit` max size and `initial_size` of pre-allocated
/// memory.
///
/// ```
/// # extern crate wasmi;
/// # use wasmi::{StackWithLimit, StackSize, FuncRef};
/// StackWithLimit::<FuncRef>::new(
/// StackSize::from_element_count(1024).into_initial(),
/// StackSize::from_element_count(2048).into_limit(),
/// );
/// ```
///
/// Unlimited
///
/// ```
/// # extern crate wasmi;
/// # use wasmi::{StackWithLimit, StackSize, RuntimeValue};
/// StackWithLimit::<RuntimeValue>::new(
/// StackSize::from_element_count(1024).into_initial(),
/// StackSize::unlimited(),
/// );
/// ```
///
/// # Errors
///
/// In debug mode, panics if `initial_size` is larger than `limit`.
pub fn new(initial_size: StackSizeInitial<T>, limit: StackSizeLimit<T>) -> StackWithLimit<T> {
let initial_size_elements = initial_size.0.element_count();
let limit_elements = limit.0.element_count();
debug_assert!(
limit >= initial_size,
limit_elements >= initial_size_elements,
"initial_size should not be larger than StackWithLimit limit"
);
use std::cmp::min;
StackWithLimit {
stack: Vec::with_capacity(initial_size),
limit: min(initial_size, limit),
stack: Vec::with_capacity(initial_size_elements.min(limit_elements)),
limit: limit_elements,
}
}
/// Create an new StackWithLimit with `limit` max size and `limit` elements pre-allocated
pub fn with_limit(limit: usize) -> StackWithLimit<T> {
StackWithLimit {
stack: Vec::with_capacity(limit),
limit: limit,
}
///
/// ```
/// # extern crate wasmi;
/// # use wasmi::{StackWithLimit, StackSize};
/// let bstack = StackWithLimit::<i32>::with_size(StackSize::from_element_count(2));
/// ```
pub fn with_size(size: StackSize<T>) -> StackWithLimit<T> {
StackWithLimit::new(StackSizeInitial(size), StackSizeLimit(size))
}
/// Attempt to push value onto stack.
@ -165,16 +78,18 @@ impl<T> StackWithLimit<T> {
///
/// Returns Err(StackOverflow) if stack is already full.
pub(crate) fn push(&mut self, value: T) -> Result<(), StackOverflow> {
debug_assert!(
self.stack.len() <= self.limit,
"Stack length should never be larger than stack limit."
);
if self.stack.len() < self.limit {
println!("limit {}", self.limit);
let ret = if self.stack.len() < self.limit {
self.stack.push(value);
Ok(())
} else {
Err(StackOverflow)
}
};
debug_assert!(
self.stack.len() <= self.limit,
"Stack length should never be larger than stack limit."
);
ret
}
pub(crate) fn pop(&mut self) -> Option<T> {
@ -190,18 +105,15 @@ impl<T> StackWithLimit<T> {
/// `bstack.get_relative_to_top(0)` gets the top of the stack
///
/// `bstack.get_relative_to_top(1)` gets the item just below the stack
///
pub(crate) fn get_relative_to_top(&self, depth: usize) -> Option<&T> {
let offset = depth + 1;
if self.stack.len() < offset {
None
} else {
// We should be cognizant of integer underflow here.
// If offset > len(), (len() - offset) will underflow.
// In debug builds, underflow panics, but in release mode, underflow is not checked.
self.stack.get(self.stack.len() - offset)
}
// Be cognizant of integer underflow and overflow here. Both are possible in this situation.
// len() is unsigned, so if len() == 0, subtraction is a problem
// depth can legally be 2^32. On a 32 bit system, adding may overflow
// overflow isn't an issue unless T is zero size
// In debug builds, underflow panics, but in release mode, underflow is not checked.
let index = self.stack.len().checked_sub(1)?.checked_sub(depth)?;
self.stack.get(index)
}
pub(crate) fn top(&self) -> Option<&T> {
@ -212,6 +124,7 @@ impl<T> StackWithLimit<T> {
self.stack.last_mut()
}
// Same as Vec::[truncate](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.truncate)
pub(crate) fn truncate(&mut self, new_size: usize) {
self.stack.truncate(new_size)
}
@ -223,26 +136,203 @@ impl<T> StackWithLimit<T> {
pub(crate) fn is_empty(&self) -> bool {
self.stack.is_empty()
}
// /// return a new empty StackWithLimit with limit equal to the amount of room
// /// this stack has available
// pub fn spare(&self) -> StackWithLimit<T> {
// // This will be used to allocate new stacks when calling into other wasm modules
// StackWithLimit::new(0, self.limit - self.len())
// }
}
// Why introduce the extra complexity of StackSizeLimit, StackSizeInitial, and StackSize?
// We want to make the user to do the correct thing using the type checker.
// Check out the excellent Rust API Guidelines (linked below) for suggestions
// about type safety in rust.
//
// By introducing the new typed arguments, we turn:
//
// ```
// pub fn new(initial_size: usize, limit: usize) -> StackWithLimit<T> { ... }
// ```
//
// into:
//
// ```
// pub fn new(initial_size: StackSizeInitial<T>, limit: StackSizeLimit<T>) -> StackWithLimit<T> { ... }
// ```
//
// https://rust-lang-nursery.github.io/api-guidelines/type-safety.html#c-custom-type
/// Type for communicating the size of some contigous container.
/// Used for constructing both [`StackSizeLimit`] and [`StackSizeInitial`].
#[derive(Eq, PartialEq, Hash, Debug)]
pub struct StackSize<T> {
num_elements: usize,
// PhantomData is a zero-sized type for keeping track of T without rustc complaining.
phantom: PhantomData<*const T>,
}
impl<T> Clone for StackSize<T> {
fn clone(&self) -> Self {
StackSize {
num_elements: self.num_elements,
phantom: PhantomData,
}
}
}
impl<T> Copy for StackSize<T> {}
impl<T> StackSize<T> {
/// Create StackSize based on number of elements.
///
/// ```
/// # extern crate wasmi;
/// # use wasmi::StackSize;
/// let ss = StackSize::<(u8, u8)>::from_element_count(10);
/// assert_eq!(ss.element_count(), 10);
/// ```
pub fn from_element_count(num_elements: usize) -> StackSize<T> {
StackSize {
num_elements,
phantom: PhantomData,
}
}
/// Compute StackSize based on allowable memory.
///
/// ```
/// # extern crate wasmi;
/// # use wasmi::StackSize;
/// let ss = StackSize::<(u8, u8)>::from_byte_count(10);
/// assert_eq!(ss.element_count(), 10 / 2);
/// ```
///
/// # Errors
///
/// In debug mode, panics if size of `T` is 0.
pub fn from_byte_count(num_bytes: usize) -> StackSize<T> {
// This debug_assert should catch logical errors.
debug_assert!(::core::mem::size_of::<T>() != 0, "That doesn't make sense.");
// In case a zero sized T still makes it into prod. We assume unlimited stack
// size instead of panicking.
let element_count = if ::core::mem::size_of::<T>() != 0 {
num_bytes / ::core::mem::size_of::<T>()
} else {
usize::MAX // Semi-relevant fun fact: Vec::<()>::new().capacity() == usize::MAX
};
StackSize::from_element_count(element_count)
}
/// Return number the of elements this StackSize indicates.
///
/// ```
/// # use wasmi::StackSize;
/// let ss = StackSize::<(u8, u8)>::from_element_count(10);
/// assert_eq!(ss.element_count(), 10);
/// ```
///
pub fn element_count(&self) -> usize {
self.num_elements
}
/// Create StackSizeLimit out of self
///
/// ```
/// # use wasmi::{StackSize, StackSizeLimit, RuntimeValue};
/// let values_limit: StackSizeLimit<RuntimeValue> = StackSize::from_element_count(1024).into_limit();
/// ```
pub fn into_limit(self) -> StackSizeLimit<T> {
StackSizeLimit(self)
}
/// Create StackSizeLimit out of self
pub fn into_initial(self) -> StackSizeInitial<T> {
StackSizeInitial(self)
}
/// Create StackSizeLimit with no upper bound.
pub fn unlimited() -> StackSizeLimit<T> {
StackSize::from_element_count(usize::MAX).into_limit()
}
}
/// Max size a stack may become.
///
/// Constructed by [`StackSize::into_limit`](into_limit) or [`StackSize::unlimited`](unlimited)
///
/// [into_limit]: type.StackSize.into_limit.html
/// [unlimited]: type.StackSize.unlimited.html
pub struct StackSizeLimit<T>(StackSize<T>);
/// Number of pre-allocated elements.
///
/// Constructed by [`StackSize::into_initial`]
///
/// [into_initial]: type.StackSize.into_initial.html
pub struct StackSizeInitial<T>(StackSize<T>);
#[cfg(test)]
mod test {
use super::StackWithLimit;
use super::{StackSize, StackSizeInitial, StackSizeLimit, StackWithLimit};
use core::usize;
#[test]
fn get_relative_to_top() {
let mut bstack = StackWithLimit::<i32>::with_limit(2);
let mut bstack = StackWithLimit::<i32>::with_size(StackSize::from_element_count(2));
assert_eq!(bstack.get_relative_to_top(0), None);
bstack.push(1).unwrap();
bstack.push(2).unwrap();
bstack.push(3).unwrap_err();
assert_eq!(bstack.get_relative_to_top(0), Some(&2));
assert_eq!(bstack.get_relative_to_top(1), Some(&1));
assert_eq!(bstack.get_relative_to_top(2), None);
assert_eq!(bstack.get_relative_to_top(3), None);
}
fn exersize(mut bstack: StackWithLimit<i32>) {
assert!(bstack.is_empty());
assert_eq!(bstack.len(), 0);
assert_eq!(bstack.top(), None);
assert_eq!(bstack.top_mut(), None);
assert_eq!(bstack.pop(), None);
bstack.push(0).unwrap();
assert!(!bstack.is_empty());
assert_eq!(bstack.len(), 1);
assert_eq!(bstack.top(), Some(&0));
assert_eq!(bstack.top_mut(), Some(&mut 0));
assert_eq!(bstack.pop(), Some(0));
bstack.push(0).unwrap();
bstack.push(0).unwrap();
bstack.push(0).unwrap();
bstack.push(0).unwrap();
assert_eq!(bstack.len(), 4);
bstack.truncate(8);
assert_eq!(bstack.len(), 4);
bstack.truncate(4);
assert_eq!(bstack.len(), 4);
bstack.truncate(2);
assert_eq!(bstack.len(), 2);
bstack.truncate(0);
assert_eq!(bstack.len(), 0);
}
#[test]
fn stack_with_limit() {
let bstack = StackWithLimit::<i32>::with_size(StackSize::from_element_count(20));
exersize(bstack);
}
// Check for integer overflow bugs
#[test]
fn practically_unlimited_stack() {
let bstack = StackWithLimit::<i32>::new(
StackSizeInitial(StackSize::from_element_count(0)),
StackSizeLimit(StackSize::from_element_count(usize::MAX)),
);
println!(
"{}",
StackSizeLimit(StackSize::<i32>::from_element_count(usize::MAX))
.0
.element_count()
);
exersize(bstack);
}
}

View File

@ -404,6 +404,7 @@ pub use self::module::{ModuleInstance, ModuleRef, ExternVal, NotStartedModuleRef
pub use self::global::{GlobalInstance, GlobalRef};
pub use self::func::{FuncInstance, FuncRef, FuncInvocation, ResumableError};
pub use self::types::{Signature, ValueType, GlobalDescriptor, TableDescriptor, MemoryDescriptor};
pub use self::common::stack::{StackWithLimit, StackSize, StackSizeLimit, StackSizeInitial};
/// WebAssembly-specific sizes and units.
pub mod memory_units {

View File

@ -8,7 +8,7 @@ use validation::context::ModuleContext;
use validation::util::Locals;
use validation::Error;
use common::stack::{StackOverflow, StackWithLimit};
use common::stack::{StackSize, StackWithLimit};
use isa;
/// Maximum number of entries in value stack per function.
@ -1348,8 +1348,8 @@ impl<'a> FunctionValidationContext<'a> {
module: module,
position: 0,
locals: locals,
value_stack: StackWithLimit::with_limit(value_stack_limit),
frame_stack: StackWithLimit::with_limit(frame_stack_limit),
value_stack: StackWithLimit::with_size(StackSize::from_element_count(value_stack_limit)),
frame_stack: StackWithLimit::with_size(StackSize::from_element_count(frame_stack_limit)),
return_type: return_type,
sink: Sink::with_instruction_capacity(size_estimate),
}