use crate::analysis::is_virtual_pointer;
use crate::analysis::unsafe_inference::is_valid_ptr;
use crate::snapshot::types::ActiveAllocation;
const MAX_READ_BYTES: usize = 4096;
const PAGE_SIZE: usize = 4096;
#[derive(Debug)]
pub struct ScanResult {
pub ptr: usize,
pub size: usize,
pub memory: Option<Vec<u8>>,
}
pub struct HeapScanner;
impl HeapScanner {
pub fn scan(allocations: &[ActiveAllocation]) -> Vec<ScanResult> {
let regions = Self::dedup_heap_regions(allocations);
regions
.iter()
.map(|&(ptr, size)| {
let memory = safe_read_memory(ptr, size);
ScanResult { ptr, size, memory }
})
.collect()
}
fn dedup_heap_regions(allocs: &[ActiveAllocation]) -> Vec<(usize, usize)> {
use std::collections::HashSet;
let mut seen = HashSet::new();
let mut regions = Vec::new();
for alloc in allocs {
if let crate::core::types::TrackKind::HeapOwner { ptr, size } = alloc.kind {
if is_virtual_pointer(ptr) {
continue;
}
let key = (ptr, size);
if seen.insert(key) {
regions.push(key);
}
}
}
regions
}
}
fn safe_read_memory(ptr: usize, size: usize) -> Option<Vec<u8>> {
if size == 0 || ptr == 0 {
return None;
}
if !is_valid_ptr(ptr) {
return None;
}
let read_size = size.min(MAX_READ_BYTES);
if !are_pages_valid(ptr, read_size) {
return None;
}
let mut buf = vec![0u8; read_size];
#[cfg(target_os = "linux")]
{
if safe_read_linux(ptr, &mut buf) {
Some(buf)
} else {
None
}
}
#[cfg(not(target_os = "linux"))]
{
if read_bytes_volatile(ptr, &mut buf) {
Some(buf)
} else {
None
}
}
}
#[cfg(target_os = "linux")]
mod linux_read {
use libc::{iovec, process_vm_readv};
pub fn safe_read_linux_local(
remote_ptr: *const libc::c_void,
local_ptr: *mut libc::c_void,
len: usize,
) -> isize {
let local_iov = iovec {
iov_base: local_ptr,
iov_len: len,
};
let remote_iov = iovec {
iov_base: remote_ptr as *mut libc::c_void,
iov_len: len,
};
unsafe { process_vm_readv(0, &local_iov, 1, &remote_iov, 1, 0) }
}
}
#[cfg(target_os = "linux")]
fn safe_read_linux(ptr: usize, buf: &mut [u8]) -> bool {
use linux_read::safe_read_linux_local;
let len = buf.len();
let result = safe_read_linux_local(
ptr as *const libc::c_void,
buf.as_mut_ptr() as *mut libc::c_void,
len,
);
result == len as isize
}
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)] fn safe_read_linux(_ptr: usize, _buf: &mut [u8]) -> bool {
false
}
#[cfg(not(target_os = "linux"))]
fn read_bytes_volatile(ptr: usize, buf: &mut [u8]) -> bool {
if !are_pages_valid(ptr, buf.len()) {
return false;
}
unsafe {
let src = ptr as *const u8;
for (i, byte) in buf.iter_mut().enumerate() {
*byte = std::ptr::read_volatile(src.add(i));
}
}
true
}
fn are_pages_valid(ptr: usize, size: usize) -> bool {
let page_start = ptr & !(PAGE_SIZE - 1);
let page_end = (ptr + size + PAGE_SIZE - 1) & !(PAGE_SIZE - 1);
let mut p = page_start;
while p < page_end {
if !is_valid_ptr(p) {
return false;
}
p += PAGE_SIZE;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::TrackKind;
#[test]
fn test_safe_read_memory_zero_size() {
assert!(safe_read_memory(0x10000, 0).is_none());
}
#[test]
fn test_safe_read_memory_null_ptr() {
assert!(safe_read_memory(0, 100).is_none());
}
#[test]
#[cfg(target_os = "macos")]
fn test_are_pages_valid_single_page() {
assert!(are_pages_valid(0x10000, 100));
}
#[test]
#[cfg(target_os = "macos")]
fn test_are_pages_valid_cross_page() {
let ptr = 0x10000;
let size = 200;
assert!(are_pages_valid(ptr, size));
}
#[test]
fn test_scan_result_creation() {
let result = ScanResult {
ptr: 0x1000,
size: 64,
memory: None,
};
assert_eq!(result.ptr, 0x1000);
assert_eq!(result.size, 64);
assert!(result.memory.is_none());
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_heap_scanner_scan_real_allocations() {
let data1 = vec![42u8; 64];
let data2 = vec![99u8; 128];
let ptr1 = data1.as_ptr() as usize;
let ptr2 = data2.as_ptr() as usize;
let allocations = vec![
ActiveAllocation {
ptr: Some(ptr1),
size: 64,
kind: TrackKind::HeapOwner {
ptr: ptr1,
size: 64,
},
allocated_at: 1000,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
},
ActiveAllocation {
ptr: Some(ptr2),
size: 128,
kind: TrackKind::HeapOwner {
ptr: ptr2,
size: 128,
},
allocated_at: 2000,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
},
];
let results = HeapScanner::scan(&allocations);
assert_eq!(results.len(), 2);
assert!(results[0].memory.is_some(), "Should read memory at ptr1");
assert!(results[1].memory.is_some(), "Should read memory at ptr2");
drop(data1);
drop(data2);
}
#[test]
fn test_heap_scanner_scan_empty_allocations() {
let allocations: Vec<ActiveAllocation> = vec![];
let results = HeapScanner::scan(&allocations);
assert!(results.is_empty());
}
#[test]
fn test_heap_scanner_scan_zero_size_allocation() {
let allocations = vec![ActiveAllocation {
ptr: Some(0x10000),
size: 0,
kind: TrackKind::HeapOwner {
ptr: 0x10000,
size: 0,
},
allocated_at: 1000,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
}];
let results = HeapScanner::scan(&allocations);
assert_eq!(results.len(), 1);
assert!(results[0].memory.is_none());
}
#[test]
#[cfg(not(target_os = "linux"))]
#[ignore = "Heap pointer addresses may exceed VIRTUAL_PTR_BASE in some CI environments"]
fn test_heap_scanner_content_preserved_after_scan() {
let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
let ptr = data.as_ptr() as usize;
let size = data.len();
let alloc = ActiveAllocation {
ptr: Some(ptr),
size,
kind: TrackKind::HeapOwner { ptr, size },
allocated_at: 1000,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
};
let results = HeapScanner::scan(&[alloc]);
assert_eq!(results.len(), 1);
let mem = results[0]
.memory
.as_ref()
.expect("Should read memory at allocated address");
assert_eq!(mem.len(), size, "Should read expected number of bytes");
drop(data);
}
}