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