-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wmemcheck support for additional allocation functions and granular memory #9641
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,18 @@ macro_rules! foreach_builtin_function { | |
// Invoked before malloc returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_malloc(vmctx: vmctx, addr: i32, len: i32) -> i32; | ||
// Invoked before calloc returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_calloc(vmctx: vmctx, addr: i32, count: i32, size: i32) -> i32; | ||
// Invoked before realloc returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_realloc(vmctx: vmctx, end_addr: i32, start_addr: i32, len: i32) -> i32; | ||
// Invoked before malloc_usable_size returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_malloc_usable_size(vmctx: vmctx, len: i32, addr: i32) -> i32; | ||
// Invoked before posix_memalign returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_posix_memalign(vmctx: vmctx, outptr: i32, size: i32) -> i32; | ||
// Invoked before the free returns. | ||
#[cfg(feature = "wmemcheck")] | ||
check_free(vmctx: vmctx, addr: i32) -> i32; | ||
|
@@ -53,19 +65,17 @@ macro_rules! foreach_builtin_function { | |
// Invoked before a store is executed. | ||
#[cfg(feature = "wmemcheck")] | ||
check_store(vmctx: vmctx, num_bytes: i32, addr: i32, offset: i32) -> i32; | ||
// Invoked after malloc is called. | ||
#[cfg(feature = "wmemcheck")] | ||
malloc_start(vmctx: vmctx); | ||
// Invoked after free is called. | ||
#[cfg(feature = "wmemcheck")] | ||
free_start(vmctx: vmctx); | ||
// Invoked when wasm stack pointer is updated. | ||
#[cfg(feature = "wmemcheck")] | ||
update_stack_pointer(vmctx: vmctx, value: i32); | ||
// Invoked before memory.grow is called. | ||
#[cfg(feature = "wmemcheck")] | ||
update_mem_size(vmctx: vmctx, num_bytes: i32); | ||
|
||
// Invoked before stuff is called. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we be more specific than "stuff" here? |
||
#[cfg(feature = "wmemcheck")] | ||
memcheck_off(vmctx: vmctx); | ||
|
||
// Drop a non-stack GC reference (eg an overwritten table entry) | ||
// once it will no longer be used again. (Note: `val` is not of type | ||
// `reference` because it needn't appear in any stack maps, as it | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1137,6 +1137,40 @@ unsafe fn check_malloc( | |
Ok(0) | ||
} | ||
|
||
// Hook for validating calloc using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
unsafe fn check_calloc(store: &mut dyn VMStore, instance: &mut Instance, addr: u32, count: u32, size: u32) -> Result<u32> { | ||
check_malloc(store, instance, addr, count * size) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a note here that since we aren't tracking undefined-ness, we don't have to care about the other semantic difference between |
||
} | ||
|
||
// Hook for validating realloc using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
unsafe fn check_realloc(store: &mut dyn VMStore, instance: &mut Instance, end_addr: u32, start_addr: u32, len: u32) -> Result<u32> { | ||
check_free(store, instance, start_addr)?; | ||
check_malloc(store, instance, end_addr, len) | ||
} | ||
|
||
// Hook for validating malloc_usable_size using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
unsafe fn check_malloc_usable_size(store: &mut dyn VMStore, instance: &mut Instance, len: u32, addr: u32) -> Result<u32> { | ||
check_free(store, instance, addr)?; | ||
check_malloc(store, instance, addr, len) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a correct implementation of |
||
} | ||
|
||
|
||
// Hook for validating posix_memalign using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
unsafe fn check_posix_memalign(store: &mut dyn VMStore, instance: &mut Instance, outptr: u32, size: u32) -> Result<u32> { | ||
for (_, entry) in instance.exports() { | ||
if let wasmtime_environ::EntityIndex::Memory(mem_index) = entry { | ||
let mem = instance.get_memory(*mem_index); | ||
let out = *(mem.base.offset(outptr as isize) as *mut u32); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We definitely need to do a bounds-check here -- could you use the safe APIs for the memory to access the out-pointer? This is also little-endian-specific code; we'll want to get a |
||
return check_malloc(store, instance, out, size) | ||
} | ||
} | ||
todo!("Why is there no memory?") | ||
} | ||
|
||
// Hook for validating free using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
unsafe fn check_free(_store: &mut dyn VMStore, instance: &mut Instance, addr: u32) -> Result<u32> { | ||
|
@@ -1158,6 +1192,30 @@ unsafe fn check_free(_store: &mut dyn VMStore, instance: &mut Instance, addr: u3 | |
Ok(0) | ||
} | ||
|
||
#[cfg(feature = "wmemcheck")] | ||
fn log_allocation_previous_to(instance: &mut Instance, addr: usize) { | ||
if let Some(wmemcheck_state) = &mut instance.wmemcheck_state { | ||
if let Some((prev_malloc, prev_len)) = wmemcheck_state.malloc_previous_to(addr) { | ||
println!("previous malloc'd range was {:#x}..{:#x}", prev_malloc, prev_malloc + prev_len); | ||
for (_, entry) in instance.exports() { | ||
if let wasmtime_environ::EntityIndex::Memory(mem_index) = entry { | ||
let mem = instance.get_memory(*mem_index); | ||
for i in 0..prev_len { | ||
if i > 0 && i % 40 == 0 { | ||
println!(); | ||
} | ||
unsafe { | ||
print!("{:02x} ", *mem.base.offset((prev_malloc + i) as isize)); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like code that could be useful for debugging, but as above with the |
||
} | ||
println!(); | ||
break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Hook for validating load using wmemcheck_state. | ||
#[cfg(feature = "wmemcheck")] | ||
fn check_load( | ||
|
@@ -1174,6 +1232,7 @@ fn check_load( | |
return Ok(0); | ||
} | ||
Err(InvalidRead { addr, len }) => { | ||
log_allocation_previous_to(instance, addr); | ||
bail!("Invalid load at addr {:#x} of size {}", addr, len); | ||
} | ||
Err(OutOfBounds { addr, len }) => { | ||
|
@@ -1203,6 +1262,7 @@ fn check_store( | |
return Ok(0); | ||
} | ||
Err(InvalidWrite { addr, len }) => { | ||
log_allocation_previous_to(instance, addr); | ||
bail!("Invalid store at addr {:#x} of size {}", addr, len) | ||
} | ||
Err(OutOfBounds { addr, len }) => { | ||
|
@@ -1216,17 +1276,8 @@ fn check_store( | |
Ok(0) | ||
} | ||
|
||
// Hook for turning wmemcheck load/store validation off when entering a malloc function. | ||
#[cfg(feature = "wmemcheck")] | ||
fn malloc_start(_store: &mut dyn VMStore, instance: &mut Instance) { | ||
if let Some(wmemcheck_state) = &mut instance.wmemcheck_state { | ||
wmemcheck_state.memcheck_off(); | ||
} | ||
} | ||
|
||
// Hook for turning wmemcheck load/store validation off when entering a free function. | ||
#[cfg(feature = "wmemcheck")] | ||
fn free_start(_store: &mut dyn VMStore, instance: &mut Instance) { | ||
fn memcheck_off(_store: &mut dyn VMStore, instance: &mut Instance) { | ||
if let Some(wmemcheck_state) = &mut instance.wmemcheck_state { | ||
wmemcheck_state.memcheck_off(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,21 @@ pub struct Wmemcheck { | |
pub flag: bool, | ||
} | ||
|
||
impl Wmemcheck { | ||
pub fn malloc_previous_to(&self, addr: usize) -> Option<(usize, usize)> { | ||
let mut best: Option<(usize, usize)> = None; | ||
for (base, len) in self.mallocs.iter() { | ||
if let Some((prev_base, _)) = best { | ||
if prev_base < *base && *base <= addr { | ||
best = Some((*base, *len)); | ||
} | ||
} else { | ||
best = Some((*base, *len)); | ||
} | ||
} | ||
best | ||
} | ||
} | ||
/// Error types for memory checker. | ||
#[derive(Debug, PartialEq)] | ||
pub enum AccessError { | ||
|
@@ -51,7 +66,10 @@ impl Wmemcheck { | |
} | ||
|
||
/// Updates memory checker memory state metadata when malloc is called. | ||
pub fn malloc(&mut self, addr: usize, len: usize) -> Result<(), AccessError> { | ||
pub fn malloc(&mut self, addr: usize, start_len: usize) -> Result<(), AccessError> { | ||
// round up to multiple of 4 | ||
let len = (start_len + 3) & !3; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we put this under an option? Perhaps "wmemcheck granularity" or similar? |
||
|
||
if !self.is_in_bounds_heap(addr, len) { | ||
return Err(AccessError::OutOfBounds { | ||
addr: addr, | ||
|
@@ -101,12 +119,12 @@ impl Wmemcheck { | |
len: len, | ||
}); | ||
} | ||
MemState::ValidToWrite => { | ||
/* MemState::ValidToWrite => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this commented out? (If the checks were causing issues, could we put the exclusion under an appropriately named and documented option?) |
||
return Err(AccessError::InvalidRead { | ||
addr: addr, | ||
len: len, | ||
}); | ||
} | ||
} */ | ||
_ => {} | ||
} | ||
} | ||
|
@@ -140,6 +158,9 @@ impl Wmemcheck { | |
|
||
/// Updates memory checker memory state metadata when free is called. | ||
pub fn free(&mut self, addr: usize) -> Result<(), AccessError> { | ||
if addr == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good find! Can we add a comment here noting the implemented semantics ( |
||
return Ok(()); | ||
} | ||
if !self.mallocs.contains_key(&addr) { | ||
return Err(AccessError::InvalidFree { addr: addr }); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment here noting where the
__wrap_*
variants come from? That's useful both for its own sake (readers' understanding) but also so that if things change in the future with the external thing causing this need, we can re-evaluate.