Browse Source
* attempt at inserting things where i think they might belong + questions * entry hook + questions * commented out all changes, doc comment errors * fix doc comment * libcalls build now!!!! * initial check_malloc_exit setup * WIP: load/store hooks * hooks added + building * added valgrind library * made wasm-valgrind accessible in wasmtime * check_malloc filled in... * move valgrind_state to an appropriate part of instance it works!!!!! * yay it's working! (?) i think?? * stack tracing in progress * errors + num bytes displayed * initial valgrind configuration * valgrind conditional some warnings fixed * conditional compilation + CLI flag finished * panic!() changed to bail!() * started adding doc comments * added memory grow hook + fixed access size handling * removed test.wasm * removed malloc_twice.wat * doc comments in spec.rs * pr feedback addressed * ran cargo fmt * addressing more feedback * Remove fuzz crate from wmemcheck. * Review feedback and test fix. * add wasmtime-wmemcheck crate to publish allowlist. * fix build without compiler features * reorder crates in publish list * Add trampolines for libcalls on s390x. * Make wasmtime-wmemcheck dep an exact version requirement. --------- Co-authored-by: iximeow <awortman@fastly.com> Co-authored-by: Chris Fallin <chris@cfallin.org> Co-authored-by: iximeow <git@iximeow.net>pull/6134/merge
ssunkin-fastly
1 year ago
committed by
GitHub
30 changed files with 1002 additions and 31 deletions
@ -0,0 +1,11 @@ |
|||
[package] |
|||
name = "wasmtime-wmemcheck" |
|||
version.workspace = true |
|||
authors.workspace = true |
|||
description = "Memcheck implementation for Wasmtime" |
|||
license = "Apache-2.0 WITH LLVM-exception" |
|||
repository = "https://github.com/bytecodealliance/wasmtime" |
|||
documentation = "https://docs.rs/wasmtime-cranelift/" |
|||
edition.workspace = true |
|||
|
|||
[dependencies] |
@ -0,0 +1,404 @@ |
|||
use std::cmp::*; |
|||
use std::collections::HashMap; |
|||
|
|||
/// Memory checker for wasm guest.
|
|||
pub struct Wmemcheck { |
|||
metadata: Vec<MemState>, |
|||
mallocs: HashMap<usize, usize>, |
|||
pub stack_pointer: usize, |
|||
max_stack_size: usize, |
|||
pub flag: bool, |
|||
} |
|||
|
|||
/// Error types for memory checker.
|
|||
#[derive(Debug, PartialEq)] |
|||
pub enum AccessError { |
|||
/// Malloc over already malloc'd memory.
|
|||
DoubleMalloc { addr: usize, len: usize }, |
|||
/// Read from uninitialized or undefined memory.
|
|||
InvalidRead { addr: usize, len: usize }, |
|||
/// Write to uninitialized memory.
|
|||
InvalidWrite { addr: usize, len: usize }, |
|||
/// Free of non-malloc'd pointer.
|
|||
InvalidFree { addr: usize }, |
|||
/// Access out of bounds of heap or stack.
|
|||
OutOfBounds { addr: usize, len: usize }, |
|||
} |
|||
|
|||
/// Memory state for memory checker.
|
|||
#[derive(Debug, Clone, PartialEq)] |
|||
pub enum MemState { |
|||
/// Unallocated memory.
|
|||
Unallocated, |
|||
/// Initialized but undefined memory.
|
|||
ValidToWrite, |
|||
/// Initialized and defined memory.
|
|||
ValidToReadWrite, |
|||
} |
|||
|
|||
impl Wmemcheck { |
|||
/// Initializes memory checker instance.
|
|||
pub fn new(mem_size: usize) -> Wmemcheck { |
|||
let metadata = vec![MemState::Unallocated; mem_size]; |
|||
let mallocs = HashMap::new(); |
|||
Wmemcheck { |
|||
metadata, |
|||
mallocs, |
|||
stack_pointer: 0, |
|||
max_stack_size: 0, |
|||
flag: true, |
|||
} |
|||
} |
|||
|
|||
/// Updates memory checker memory state metadata when malloc is called.
|
|||
pub fn malloc(&mut self, addr: usize, len: usize) -> Result<(), AccessError> { |
|||
if !self.is_in_bounds_heap(addr, len) { |
|||
return Err(AccessError::OutOfBounds { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
for i in addr..addr + len { |
|||
match self.metadata[i] { |
|||
MemState::ValidToWrite => { |
|||
return Err(AccessError::DoubleMalloc { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
MemState::ValidToReadWrite => { |
|||
return Err(AccessError::DoubleMalloc { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
_ => {} |
|||
} |
|||
} |
|||
for i in addr..addr + len { |
|||
self.metadata[i] = MemState::ValidToWrite; |
|||
} |
|||
self.mallocs.insert(addr, len); |
|||
Ok(()) |
|||
} |
|||
|
|||
/// Updates memory checker memory state metadata when a load occurs.
|
|||
pub fn read(&mut self, addr: usize, len: usize) -> Result<(), AccessError> { |
|||
if !self.flag { |
|||
return Ok(()); |
|||
} |
|||
if !(self.is_in_bounds_stack(addr, len) || self.is_in_bounds_heap(addr, len)) { |
|||
return Err(AccessError::OutOfBounds { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
for i in addr..addr + len { |
|||
match self.metadata[i] { |
|||
MemState::Unallocated => { |
|||
return Err(AccessError::InvalidRead { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
MemState::ValidToWrite => { |
|||
return Err(AccessError::InvalidRead { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
_ => {} |
|||
} |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
/// Updates memory checker memory state metadata when a store occurs.
|
|||
pub fn write(&mut self, addr: usize, len: usize) -> Result<(), AccessError> { |
|||
if !self.flag { |
|||
return Ok(()); |
|||
} |
|||
if !(self.is_in_bounds_stack(addr, len) || self.is_in_bounds_heap(addr, len)) { |
|||
return Err(AccessError::OutOfBounds { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
for i in addr..addr + len { |
|||
if let MemState::Unallocated = self.metadata[i] { |
|||
return Err(AccessError::InvalidWrite { |
|||
addr: addr, |
|||
len: len, |
|||
}); |
|||
} |
|||
} |
|||
for i in addr..addr + len { |
|||
self.metadata[i] = MemState::ValidToReadWrite; |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
/// Updates memory checker memory state metadata when free is called.
|
|||
pub fn free(&mut self, addr: usize) -> Result<(), AccessError> { |
|||
if !self.mallocs.contains_key(&addr) { |
|||
return Err(AccessError::InvalidFree { addr: addr }); |
|||
} |
|||
let len = self.mallocs[&addr]; |
|||
for i in addr..addr + len { |
|||
if let MemState::Unallocated = self.metadata[i] { |
|||
return Err(AccessError::InvalidFree { addr: addr }); |
|||
} |
|||
} |
|||
self.mallocs.remove(&addr); |
|||
for i in addr..addr + len { |
|||
self.metadata[i] = MemState::Unallocated; |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
fn is_in_bounds_heap(&self, addr: usize, len: usize) -> bool { |
|||
self.max_stack_size <= addr && addr + len <= self.metadata.len() |
|||
} |
|||
|
|||
fn is_in_bounds_stack(&self, addr: usize, len: usize) -> bool { |
|||
self.stack_pointer <= addr && addr + len < self.max_stack_size |
|||
} |
|||
|
|||
/// Updates memory checker metadata when stack pointer is updated.
|
|||
pub fn update_stack_pointer(&mut self, new_sp: usize) -> Result<(), AccessError> { |
|||
if new_sp > self.max_stack_size { |
|||
return Err(AccessError::OutOfBounds { |
|||
addr: self.stack_pointer, |
|||
len: new_sp - self.stack_pointer, |
|||
}); |
|||
} else if new_sp < self.stack_pointer { |
|||
for i in new_sp..self.stack_pointer + 1 { |
|||
self.metadata[i] = MemState::ValidToReadWrite; |
|||
} |
|||
} else { |
|||
for i in self.stack_pointer..new_sp { |
|||
self.metadata[i] = MemState::Unallocated; |
|||
} |
|||
} |
|||
self.stack_pointer = new_sp; |
|||
Ok(()) |
|||
} |
|||
|
|||
/// Turns memory checking on.
|
|||
pub fn memcheck_on(&mut self) { |
|||
self.flag = true; |
|||
} |
|||
|
|||
/// Turns memory checking off.
|
|||
pub fn memcheck_off(&mut self) { |
|||
self.flag = false; |
|||
} |
|||
|
|||
/// Initializes stack and stack pointer in memory checker metadata.
|
|||
pub fn set_stack_size(&mut self, stack_size: usize) { |
|||
self.max_stack_size = stack_size + 1; |
|||
// TODO: temporary solution to initialize the entire stack
|
|||
// while keeping stack tracing plumbing in place
|
|||
self.stack_pointer = stack_size; |
|||
let _ = self.update_stack_pointer(0); |
|||
} |
|||
|
|||
/// Updates memory checker metadata size when memory.grow is called.
|
|||
pub fn update_mem_size(&mut self, num_bytes: usize) { |
|||
let to_append = vec![MemState::Unallocated; num_bytes]; |
|||
self.metadata.extend(to_append); |
|||
} |
|||
} |
|||
|
|||
#[test] |
|||
fn basic_wmemcheck() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
wmemcheck_state.set_stack_size(1024); |
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert!(wmemcheck_state.write(0x1000, 4).is_ok()); |
|||
assert!(wmemcheck_state.read(0x1000, 4).is_ok()); |
|||
assert_eq!(wmemcheck_state.mallocs, HashMap::from([(0x1000, 32)])); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
assert!(wmemcheck_state.mallocs.is_empty()); |
|||
} |
|||
|
|||
#[test] |
|||
fn read_before_initializing() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.read(0x1000, 4), |
|||
Err(AccessError::InvalidRead { |
|||
addr: 0x1000, |
|||
len: 4 |
|||
}) |
|||
); |
|||
assert!(wmemcheck_state.write(0x1000, 4).is_ok()); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
} |
|||
|
|||
#[test] |
|||
fn use_after_free() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert!(wmemcheck_state.write(0x1000, 4).is_ok()); |
|||
assert!(wmemcheck_state.write(0x1000, 4).is_ok()); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.write(0x1000, 4), |
|||
Err(AccessError::InvalidWrite { |
|||
addr: 0x1000, |
|||
len: 4 |
|||
}) |
|||
); |
|||
} |
|||
|
|||
#[test] |
|||
fn double_free() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert!(wmemcheck_state.write(0x1000, 4).is_ok()); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.free(0x1000), |
|||
Err(AccessError::InvalidFree { addr: 0x1000 }) |
|||
); |
|||
} |
|||
|
|||
#[test] |
|||
fn out_of_bounds_malloc() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert_eq!( |
|||
wmemcheck_state.malloc(640 * 1024, 1), |
|||
Err(AccessError::OutOfBounds { |
|||
addr: 640 * 1024, |
|||
len: 1 |
|||
}) |
|||
); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(640 * 1024 - 10, 15), |
|||
Err(AccessError::OutOfBounds { |
|||
addr: 640 * 1024 - 10, |
|||
len: 15 |
|||
}) |
|||
); |
|||
assert!(wmemcheck_state.mallocs.is_empty()); |
|||
} |
|||
|
|||
#[test] |
|||
fn out_of_bounds_read() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(640 * 1024 - 24, 24).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.read(640 * 1024 - 24, 25), |
|||
Err(AccessError::OutOfBounds { |
|||
addr: 640 * 1024 - 24, |
|||
len: 25 |
|||
}) |
|||
); |
|||
} |
|||
|
|||
#[test] |
|||
fn double_malloc() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(0x1000, 32), |
|||
Err(AccessError::DoubleMalloc { |
|||
addr: 0x1000, |
|||
len: 32 |
|||
}) |
|||
); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(0x1002, 32), |
|||
Err(AccessError::DoubleMalloc { |
|||
addr: 0x1002, |
|||
len: 32 |
|||
}) |
|||
); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
} |
|||
|
|||
#[test] |
|||
fn error_type() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
assert!(wmemcheck_state.malloc(0x1000, 32).is_ok()); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(0x1000, 32), |
|||
Err(AccessError::DoubleMalloc { |
|||
addr: 0x1000, |
|||
len: 32 |
|||
}) |
|||
); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(640 * 1024, 32), |
|||
Err(AccessError::OutOfBounds { |
|||
addr: 640 * 1024, |
|||
len: 32 |
|||
}) |
|||
); |
|||
assert!(wmemcheck_state.free(0x1000).is_ok()); |
|||
} |
|||
|
|||
#[test] |
|||
fn update_sp_no_error() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
wmemcheck_state.set_stack_size(1024); |
|||
assert!(wmemcheck_state.update_stack_pointer(768).is_ok()); |
|||
assert_eq!(wmemcheck_state.stack_pointer, 768); |
|||
assert!(wmemcheck_state.malloc(1024 * 2, 32).is_ok()); |
|||
assert!(wmemcheck_state.free(1024 * 2).is_ok()); |
|||
assert!(wmemcheck_state.update_stack_pointer(896).is_ok()); |
|||
assert_eq!(wmemcheck_state.stack_pointer, 896); |
|||
assert!(wmemcheck_state.update_stack_pointer(1024).is_ok()); |
|||
} |
|||
|
|||
#[test] |
|||
fn bad_stack_malloc() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
wmemcheck_state.set_stack_size(1024); |
|||
|
|||
assert!(wmemcheck_state.update_stack_pointer(0).is_ok()); |
|||
assert_eq!(wmemcheck_state.stack_pointer, 0); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(512, 32), |
|||
Err(AccessError::OutOfBounds { addr: 512, len: 32 }) |
|||
); |
|||
assert_eq!( |
|||
wmemcheck_state.malloc(1022, 32), |
|||
Err(AccessError::OutOfBounds { |
|||
addr: 1022, |
|||
len: 32 |
|||
}) |
|||
); |
|||
} |
|||
|
|||
#[test] |
|||
fn stack_full_empty() { |
|||
let mut wmemcheck_state = Wmemcheck::new(640 * 1024); |
|||
|
|||
wmemcheck_state.set_stack_size(1024); |
|||
|
|||
assert!(wmemcheck_state.update_stack_pointer(0).is_ok()); |
|||
assert_eq!(wmemcheck_state.stack_pointer, 0); |
|||
assert!(wmemcheck_state.update_stack_pointer(1024).is_ok()); |
|||
assert_eq!(wmemcheck_state.stack_pointer, 1024) |
|||
} |
|||
|
|||
#[test] |
|||
fn from_test_program() { |
|||
let mut wmemcheck_state = Wmemcheck::new(1024 * 1024 * 128); |
|||
wmemcheck_state.set_stack_size(70864); |
|||
assert!(wmemcheck_state.write(70832, 1).is_ok()); |
|||
assert!(wmemcheck_state.read(1138, 1).is_ok()); |
|||
} |
@ -0,0 +1,8 @@ |
|||
|
|||
Wmemcheck provides debug output for invalid mallocs, reads, and writes. |
|||
|
|||
How to use: |
|||
1. When building Wasmtime, add the CLI flag "--features wmemcheck" to compile with wmemcheck configured. |
|||
> cargo build --features wmemcheck |
|||
2. When running your wasm module, add the CLI flag "--wmemcheck". |
|||
> wasmtime run --wmemcheck test.wasm |
Loading…
Reference in new issue