use std::ptr;
unsafe fn alloc() -> &'static compatmalloc::__test_support::HardenedAllocator {
compatmalloc::__test_support::ensure_initialized();
compatmalloc::__test_support::allocator()
}
fn expect_abort_subprocess(scenario_name: &str, expected_msg: &str) {
let exe = std::env::current_exe().expect("cannot determine test binary path");
let output = std::process::Command::new(&exe)
.env("COMPATMALLOC_HARDENING_SCENARIO", scenario_name)
.arg("--exact")
.arg("scenario_driver")
.arg("--nocapture")
.env("RUST_TEST_THREADS", "1")
.output()
.expect("failed to spawn subprocess");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"subprocess for scenario '{}' should have been killed by a signal, \
but exited successfully. stderr:\n{}",
scenario_name,
stderr
);
assert!(
stderr.contains(expected_msg),
"subprocess for scenario '{}' stderr does not contain '{}'. \
Full stderr:\n{}",
scenario_name,
expected_msg,
stderr
);
}
#[test]
fn scenario_driver() {
let scenario = match std::env::var("COMPATMALLOC_HARDENING_SCENARIO") {
Ok(s) => s,
Err(_) => return, };
match scenario.as_str() {
"double_free" => scenario_double_free(),
"canary_corruption" => scenario_canary_corruption(),
"invalid_free_garbage" => scenario_invalid_free_garbage(),
"invalid_free_stack" => scenario_invalid_free_stack(),
_ => panic!("unknown scenario: {}", scenario),
}
}
fn scenario_double_free() {
unsafe {
let a = alloc();
let p = a.malloc(64);
assert!(!p.is_null());
a.free(p);
a.free(p);
}
unreachable!("double free was not detected");
}
fn scenario_canary_corruption() {
unsafe {
let a = alloc();
let requested = 17;
let p = a.malloc(requested);
assert!(!p.is_null());
let mut extras = [ptr::null_mut(); 64];
for slot in extras.iter_mut() {
*slot = a.malloc(requested);
assert!(!slot.is_null());
}
let after = p.add(requested);
after.write(0x00);
a.free(p);
for &q in &extras {
a.free(q);
}
}
unreachable!("canary corruption was not detected");
}
#[allow(clippy::manual_dangling_ptr)]
fn scenario_invalid_free_garbage() {
unsafe {
let a = alloc();
a.free(0x1 as *mut u8);
}
unreachable!("invalid free of garbage pointer was not detected");
}
fn scenario_invalid_free_stack() {
unsafe {
let a = alloc();
let mut stack_var: u64 = 0xDEAD;
a.free(&mut stack_var as *mut u64 as *mut u8);
}
unreachable!("invalid free of stack pointer was not detected");
}
#[test]
fn double_free_detected() {
expect_abort_subprocess("double_free", "double free detected");
}
#[test]
fn invalid_free_garbage_detected() {
expect_abort_subprocess("invalid_free_garbage", "free() called on invalid pointer");
}
#[test]
fn invalid_free_stack_detected() {
expect_abort_subprocess("invalid_free_stack", "free() called on invalid pointer");
}
#[test]
#[cfg(feature = "canaries")]
fn canary_corruption_detected() {
expect_abort_subprocess("canary_corruption", "canary corrupted");
}
#[test]
#[cfg(all(feature = "poison-on-free", feature = "quarantine"))]
fn freed_memory_is_poisoned() {
const POISON: u8 = 0xFE;
unsafe {
let a = alloc();
let size = 64;
let p = a.malloc(size);
assert!(!p.is_null());
let mut extras = [ptr::null_mut(); 64];
for slot in extras.iter_mut() {
*slot = a.malloc(size);
assert!(!slot.is_null());
}
ptr::write_bytes(p, 0xAA, size);
a.free(p);
for &q in &extras {
a.free(q);
}
let slice = std::slice::from_raw_parts(p, size);
assert!(
slice.iter().all(|&b| b == POISON),
"freed memory should be poisoned with 0x{:02X}, \
but found: {:02X?}",
POISON,
&slice[..8.min(size)]
);
}
}
#[test]
#[cfg(all(
feature = "poison-on-free",
feature = "quarantine",
not(feature = "zero-on-free")
))]
fn freed_memory_poison_full_slot() {
const POISON: u8 = 0xFE;
unsafe {
let a = alloc();
let requested = 1;
let p = a.malloc(requested);
assert!(!p.is_null());
let slot_size = a.usable_size(p);
assert!(
slot_size >= requested,
"usable_size {} < requested {}",
slot_size,
requested
);
let mut extras = [ptr::null_mut(); 64];
for slot in extras.iter_mut() {
*slot = a.malloc(requested);
assert!(!slot.is_null());
}
ptr::write_bytes(p, 0xAA, slot_size);
a.free(p);
for &q in &extras {
a.free(q);
}
let slice = std::slice::from_raw_parts(p, slot_size);
assert!(
slice.iter().all(|&b| b == POISON),
"full slot not poisoned: first 16 bytes = {:02X?}",
&slice[..16.min(slot_size)]
);
}
}
#[test]
#[cfg(feature = "quarantine")]
fn quarantine_prevents_immediate_reuse() {
unsafe {
let a = alloc();
let p = a.malloc(64);
assert!(!p.is_null());
let p_addr = p as usize;
a.free(p);
let q = a.malloc(64);
assert!(!q.is_null());
if q as usize == p_addr {
eprintln!(
"WARNING: quarantine_prevents_immediate_reuse: got same address \
(quarantine may have been full or slot randomization returned same slot)"
);
}
a.free(q);
}
}
#[test]
fn metadata_tracks_freed_state() {
unsafe {
let a = alloc();
let p = a.malloc(100);
assert!(!p.is_null());
let meta_before = a.get_metadata(p);
assert!(
meta_before.is_some(),
"metadata should exist for a live allocation"
);
assert!(
!meta_before.unwrap().is_freed(),
"live allocation should not be marked as freed"
);
a.free(p);
#[cfg(feature = "quarantine")]
{
let meta_after = a.get_metadata(p);
if let Some(m) = meta_after {
assert!(
m.is_freed(),
"freed allocation metadata should have freed flag set"
);
}
}
}
}
#[test]
fn metadata_records_requested_size() {
unsafe {
let a = alloc();
for &size in &[1usize, 16, 64, 100, 256, 1024, 4096] {
let p = a.malloc(size);
assert!(!p.is_null());
let meta = a.get_metadata(p);
assert!(meta.is_some(), "metadata missing for malloc({})", size);
assert_eq!(
meta.unwrap().requested_size,
size,
"metadata requested_size mismatch for malloc({})",
size
);
a.free(p);
}
}
}
#[test]
fn large_calloc_reuse_returns_zeroed_data() {
unsafe {
let a = alloc();
let size = 32768;
let p = a.malloc(size);
assert!(!p.is_null());
core::ptr::write_bytes(p, 0xAB, size);
a.free(p);
let q = a.calloc(1, size);
assert!(!q.is_null());
let slice = core::slice::from_raw_parts(q, size);
let non_zero = slice.iter().filter(|&&b| b != 0).count();
assert_eq!(
non_zero, 0,
"large calloc reuse: expected all-zero pages, found {} non-zero bytes",
non_zero
);
a.free(q);
}
}
#[test]
fn large_alloc_cross_thread_does_not_leak_data() {
unsafe {
let a = alloc();
let size = 32768;
let p = a.malloc(size);
assert!(!p.is_null());
core::ptr::write_bytes(p, 0xAB, size);
a.free(p);
let p2 = a.malloc(size * 2);
assert!(!p2.is_null());
a.free(p2);
let q = a.malloc(size);
assert!(!q.is_null());
let slice = core::slice::from_raw_parts(q, size);
let non_zero = slice.iter().filter(|&&b| b != 0).count();
assert_eq!(
non_zero, 0,
"large cross-thread reuse: expected zero-filled pages after eviction, \
found {} non-zero bytes",
non_zero
);
a.free(q);
}
}