use crate::Rc;
use crate::{AllocError, MemType};
use core::alloc::Layout;
use core::fmt::{Debug, Formatter};
use core::ptr::NonNull;
use core::slice;
pub trait WasmMemoryAllocator {
fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, AllocError>;
fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
layout: Layout,
) -> Result<NonNull<u8>, AllocError>;
fn deallocate(&self, ptr: NonNull<u8>, layout: Layout);
}
impl<T: WasmMemoryAllocator> Rc<T> {
pub fn into_wasm_memory_allocator(self) -> Rc<dyn WasmMemoryAllocator>
where
T: WasmMemoryAllocator + 'static,
{
unsafe { self.into_dyn(|x| x as &dyn WasmMemoryAllocator) }
}
}
pub struct Memory {
ptr: *mut u8,
size: usize,
ty: MemType,
allocator: Option<Rc<dyn WasmMemoryAllocator>>,
}
impl Debug for Memory {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Memory").finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryError {
OutOfBounds,
OutOfMemory,
AllocationFailed,
PageTooSmall,
}
impl From<AllocError> for MemoryError {
fn from(e: AllocError) -> MemoryError {
match e {
AllocError::AllocationFailed => MemoryError::AllocationFailed,
AllocError::OutOfMemory => MemoryError::OutOfMemory,
AllocError::PageTooSmall => MemoryError::PageTooSmall,
}
}
}
impl Default for Memory {
fn default() -> Self {
Memory::zero()
}
}
impl Memory {
pub fn zero() -> Memory {
Memory {
ptr: core::ptr::null_mut(),
size: 0,
ty: MemType::zero(),
allocator: None,
}
}
pub fn new(ty: MemType, allocator: Rc<dyn WasmMemoryAllocator>) -> Result<Memory, AllocError> {
let size = if let Some(size) = (ty.min() as u64).checked_mul(ty.page_size() as u64) {
if size > usize::MAX as u64 {
return Err(AllocError::AllocationFailed);
} else {
size as usize
}
} else {
return Err(AllocError::AllocationFailed);
};
let ptr = if size == 0 {
core::ptr::null_mut()
} else {
let ptr = allocator
.allocate(
Layout::from_size_align(size, ty.page_alignment())
.map_err(|_| AllocError::AllocationFailed)?,
)?
.as_ptr();
unsafe {
ptr.write_bytes(0, size);
}
ptr
};
Ok(Memory {
ptr,
size,
ty,
allocator: Some(allocator),
})
}
}
impl Memory {
#[inline]
fn check_in_bounds(&self, addr: usize, size: usize) -> Result<(), MemoryError> {
if size > self.size || addr > self.size - size {
Err(MemoryError::OutOfBounds)
} else {
Ok(())
}
}
#[inline]
pub fn effective_address(base: u32, offset: u32) -> Result<usize, MemoryError> {
usize::try_from(base as u64 + offset as u64).map_err(|_| MemoryError::OutOfBounds)
}
pub fn store_u8(&self, addr: usize, i: u8) -> Result<(), MemoryError> {
self.check_in_bounds(addr, 1)?;
unsafe {
self.ptr.add(addr).write(i);
}
Ok(())
}
pub fn store_u16(&self, addr: usize, i: u16) -> Result<(), MemoryError> {
self.check_in_bounds(addr, 2)?;
unsafe {
self.ptr.add(addr).cast::<u16>().write_unaligned(i);
}
Ok(())
}
pub fn store_u32(&self, addr: usize, i: u32) -> Result<(), MemoryError> {
self.check_in_bounds(addr, 4)?;
unsafe {
self.ptr.add(addr).cast::<u32>().write_unaligned(i);
}
Ok(())
}
pub fn store_u64(&self, addr: usize, i: u64) -> Result<(), MemoryError> {
self.check_in_bounds(addr, 8)?;
unsafe {
self.ptr.add(addr).cast::<u64>().write_unaligned(i);
}
Ok(())
}
pub fn store(&self, addr: usize, data: &[u8]) -> Result<(), MemoryError> {
self.check_in_bounds(addr, data.len())?;
if data.is_empty() {
return Ok(());
}
unsafe {
data.as_ptr().copy_to(self.ptr.add(addr), data.len());
}
Ok(())
}
pub fn load_u8(&self, addr: usize) -> Result<u8, MemoryError> {
self.check_in_bounds(addr, 1)?;
unsafe { Ok(self.ptr.add(addr).read()) }
}
pub fn load_u16(&self, addr: usize) -> Result<u16, MemoryError> {
self.check_in_bounds(addr, 2)?;
unsafe { Ok(self.ptr.add(addr).cast::<u16>().read_unaligned()) }
}
pub fn load_u32(&self, addr: usize) -> Result<u32, MemoryError> {
self.check_in_bounds(addr, 4)?;
unsafe { Ok(self.ptr.add(addr).cast::<u32>().read_unaligned()) }
}
pub fn load_u64(&self, addr: usize) -> Result<u64, MemoryError> {
self.check_in_bounds(addr, 8)?;
unsafe { Ok(self.ptr.add(addr).cast::<u64>().read_unaligned()) }
}
pub fn load(&self, addr: usize, len: usize) -> Result<&[u8], MemoryError> {
self.check_in_bounds(addr, len)?;
if len == 0 {
return Ok(&[]);
}
Ok(unsafe { core::slice::from_raw_parts(self.ptr.add(addr), len) })
}
pub fn grow(&mut self, n: u32) -> Result<u32, MemoryError> {
let old_pages = self.size();
let total_pages = old_pages.checked_add(n).ok_or(MemoryError::OutOfMemory)?;
if !self.ty.can_hold(total_pages) {
return Err(MemoryError::OutOfMemory);
}
let allocator = self.allocator.as_ref().ok_or(MemoryError::OutOfMemory)?;
if n == 0 {
return Ok(old_pages);
}
let old_size = self.size;
let new_size = usize::try_from(u64::from(total_pages) * self.ty.page_size() as u64)
.map_err(|_| MemoryError::OutOfMemory)?;
let new_layout = Layout::from_size_align(new_size, self.ty.page_alignment())
.map_err(|_| MemoryError::AllocationFailed)?;
let ptr = if old_size == 0 {
allocator.allocate(new_layout)?
} else {
allocator.reallocate(
NonNull::new(self.ptr).ok_or(MemoryError::OutOfMemory)?,
Layout::from_size_align(old_size, self.ty.page_alignment())
.map_err(|_| MemoryError::AllocationFailed)?,
new_layout,
)?
};
unsafe {
ptr.as_ptr()
.add(old_size)
.write_bytes(0, new_size - old_size);
}
self.ptr = ptr.as_ptr();
self.size = new_size;
Ok(old_pages)
}
pub fn mem_type(&self) -> MemType {
self.ty
}
pub fn size(&self) -> u32 {
(self.size / self.ty.page_size()) as u32
}
pub fn is_zero(&self) -> bool {
self.ptr.is_null() && self.allocator.is_none()
}
pub fn get_slice(&self) -> &[u8] {
if self.ptr.is_null() {
&[]
} else {
unsafe { slice::from_raw_parts(self.ptr, self.size) }
}
}
}
impl Drop for Memory {
fn drop(&mut self) {
if !self.ptr.is_null() {
let allocator = self.allocator.take().unwrap();
allocator.deallocate(
NonNull::new(self.ptr).unwrap(),
Layout::from_size_align(self.size, self.ty.page_alignment()).unwrap(),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemPageSize;
extern crate std;
struct TestAllocator;
impl WasmMemoryAllocator for TestAllocator {
fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, AllocError> {
assert_ne!(layout.size(), 0);
unsafe { NonNull::new(std::alloc::alloc(layout)).ok_or(AllocError::AllocationFailed) }
}
fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
layout: Layout,
) -> Result<NonNull<u8>, AllocError> {
assert_ne!(old_layout.size(), 0);
assert_ne!(layout.size(), 0);
unsafe {
let new_ptr = std::alloc::realloc(ptr.as_ptr(), old_layout, layout.size());
NonNull::new(new_ptr).ok_or(AllocError::AllocationFailed)
}
}
fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
assert_ne!(layout.size(), 0);
unsafe { std::alloc::dealloc(ptr.as_ptr(), layout) }
}
}
fn allocator() -> Rc<dyn WasmMemoryAllocator> {
Rc::new(TestAllocator).unwrap().into_wasm_memory_allocator()
}
fn mem_type(min: u32, max: Option<u32>) -> MemType {
MemType {
initial_pages: min,
max_pages: max,
page_size: MemPageSize::_65536,
}
}
#[test]
fn zero_memory_is_zero() {
let mem = Memory::zero();
assert!(mem.is_zero());
assert_eq!(mem.size(), 0);
}
#[test]
fn new_allocates_and_reports_size() {
let mem = Memory::new(mem_type(2, Some(4)), allocator()).unwrap();
assert!(!mem.is_zero());
assert_eq!(mem.size(), 2);
assert_eq!(mem.mem_type().min(), 2);
assert!(mem.get_slice().iter().all(|&b| b == 0));
assert_eq!(mem.get_slice().len(), 2 * MemPageSize::_65536.size());
}
#[test]
fn zero_page_memory_allocates_lazily() {
let mut mem = Memory::new(mem_type(0, None), allocator()).unwrap();
assert!(!mem.is_zero());
assert_eq!(mem.size(), 0);
assert!(mem.get_slice().is_empty());
assert_eq!(mem.load(0, 0), Ok(&[][..]));
assert_eq!(mem.store(0, &[]), Ok(()));
assert_eq!(mem.load(1, 0), Err(MemoryError::OutOfBounds));
assert_eq!(mem.store(1, &[]), Err(MemoryError::OutOfBounds));
assert_eq!(mem.grow(0), Ok(0));
assert_eq!(mem.grow(1), Ok(0));
assert_eq!(mem.size(), 1);
assert_eq!(mem.grow(0), Ok(1));
assert_eq!(mem.get_slice().len(), MemPageSize::_65536.size());
assert!(mem.get_slice().iter().all(|&b| b == 0));
assert_eq!(mem.load(0, 1), Ok(&[0][..]));
assert_eq!(mem.store(0, &[0xA5]), Ok(()));
assert_eq!(mem.load(0, 1), Ok(&[0xA5][..]));
}
#[test]
fn grow_extends_and_zeroes() {
let mut mem = Memory::new(mem_type(1, Some(4)), allocator()).unwrap();
mem.store_u32(0, 0xAABB_CCDD).unwrap();
let old_pages = mem.grow(2).unwrap();
assert_eq!(old_pages, 1);
assert_eq!(mem.size(), 3);
assert_eq!(mem.load_u32(0).unwrap(), 0xAABB_CCDD);
let first_new_byte = MemPageSize::_65536.size();
assert_eq!(mem.load_u8(first_new_byte).unwrap(), 0);
}
#[test]
fn grow_past_max_fails() {
let mut mem = Memory::new(mem_type(1, Some(2)), allocator()).unwrap();
assert_eq!(mem.grow(2), Err(MemoryError::OutOfMemory));
assert_eq!(mem.size(), 1);
}
#[test]
fn grow_overflow_page_count_fails() {
let mut mem = Memory::new(mem_type(1, None), allocator()).unwrap();
assert_eq!(mem.grow(u32::MAX), Err(MemoryError::OutOfMemory));
}
#[test]
fn grow_zero_memory_fails() {
let mut mem = Memory::zero();
assert_eq!(mem.grow(0), Err(MemoryError::OutOfMemory));
assert_eq!(mem.grow(1), Err(MemoryError::OutOfMemory));
}
#[test]
fn load_store_out_of_bounds() {
let mem = Memory::new(mem_type(1, None), allocator()).unwrap();
let size = MemPageSize::_65536.size();
assert_eq!(mem.load_u32(size - 3), Err(MemoryError::OutOfBounds));
assert_eq!(mem.store_u64(size - 4, 0), Err(MemoryError::OutOfBounds));
assert_eq!(mem.load(size - 1, 4), Err(MemoryError::OutOfBounds));
}
#[test]
fn effective_address_rejects_overflow_on_32bit() {
assert_eq!(Memory::effective_address(10, 20).unwrap(), 30);
}
#[test]
fn memory_error_from_alloc_error() {
assert_eq!(
MemoryError::from(AllocError::AllocationFailed),
MemoryError::AllocationFailed
);
assert_eq!(
MemoryError::from(AllocError::OutOfMemory),
MemoryError::OutOfMemory
);
assert_eq!(
MemoryError::from(AllocError::PageTooSmall),
MemoryError::PageTooSmall
);
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
use crate::Allocator;
use crate::MemPageSize;
extern crate std;
use crate::test_support::RustSystemAllocator;
#[kani::proof]
fn proof_store_load_correctness() {
let alloc = RustSystemAllocator;
let size = 64;
let ptr = unsafe {
alloc
.alloc(Layout::from_size_align(size, (MemPageSize::_65536).alignment()).unwrap())
.unwrap()
};
let mem = Memory {
ptr,
size,
ty: MemType::zero(),
allocator: None,
};
let addr: usize = kani::any();
kani::assume(addr <= size - 8);
let val_u8: u8 = kani::any();
mem.store_u8(addr, val_u8).unwrap();
assert_eq!(mem.load_u8(addr).unwrap(), val_u8, "u8 round-trip failed");
let val_u16: u16 = kani::any();
mem.store_u16(addr, val_u16).unwrap();
assert_eq!(
mem.load_u16(addr).unwrap(),
val_u16,
"u16 round-trip failed"
);
let val_u32: u32 = kani::any();
mem.store_u32(addr, val_u32).unwrap();
assert_eq!(
mem.load_u32(addr).unwrap(),
val_u32,
"u32 round-trip failed"
);
let val_u64: u64 = kani::any();
mem.store_u64(addr, val_u64).unwrap();
assert_eq!(
mem.load_u64(addr).unwrap(),
val_u64,
"u64 round-trip failed"
);
assert!(
mem.store_u32(size - 2, 0).is_err(),
"Out-of-bounds store must fail"
);
assert!(
mem.store_u32(size, 0).is_err(),
"Store at boundary must fail"
);
assert!(
mem.load_u32(size - 2).is_err(),
"Out-of-bounds load must fail"
);
assert!(mem.load_u32(size).is_err(), "Load at boundary must fail");
let overflow_addr = usize::MAX - 1;
assert!(
mem.store_u32(overflow_addr, 0).is_err(),
"Overflow address must be rejected"
);
assert!(
mem.load_u32(overflow_addr).is_err(),
"Overflow address must be rejected"
);
if addr <= isize::MAX as usize {
let offset = addr as isize;
assert!(offset >= 0, "Valid addr converts to non-negative offset");
assert!(offset as usize == addr, "isize conversion must be lossless");
}
core::mem::forget(mem);
unsafe {
alloc.dealloc(
ptr,
Layout::from_size_align(size, (MemPageSize::_65536).alignment()).unwrap(),
)
};
}
#[kani::proof]
fn proof_byte_slice_operations() {
let alloc = RustSystemAllocator;
let size = 16;
let ptr = unsafe {
alloc
.alloc(Layout::from_size_align(size, (MemPageSize::_65536).alignment()).unwrap())
.unwrap()
};
let mem = Memory {
ptr,
size,
ty: MemType::zero(),
allocator: None,
};
let addr: usize = kani::any();
kani::assume(addr <= size - 4);
let data: [u8; 4] = kani::any();
mem.store(addr, &data).unwrap();
let loaded = mem.load(addr, 4).unwrap();
assert_eq!(loaded, &data, "Byte slice round-trip failed");
mem.store(0, &[]).unwrap();
assert_eq!(mem.load(0, 0).unwrap().len(), 0, "Empty slice must work");
assert!(
mem.store(size - 2, &[1, 2, 3, 4]).is_err(),
"Out-of-bounds store must fail"
);
assert!(
mem.load(size - 2, 4).is_err(),
"Out-of-bounds load must fail"
);
core::mem::forget(mem);
unsafe {
alloc.dealloc(
ptr,
Layout::from_size_align(size, (MemPageSize::_65536).alignment()).unwrap(),
)
};
}
#[kani::proof]
fn proof_effective_address_no_wrap() {
let base: u32 = kani::any();
let offset: u32 = kani::any();
let ea = Memory::effective_address(base, offset).unwrap();
assert_eq!(ea as u64, base as u64 + offset as u64);
}
}