use std::process::Command;
const MARKER: &str = "RUSTY_ALLOC_CORRUPT_CHILD";
const SURVIVED: &str = "CHILD-SURVIVED-CORRUPTION";
#[cfg(unix)]
const SIGABRT: i32 = 6;
const N: usize = 2048;
const SZ: usize = 64;
fn build_poisonable_free_list(ps: &mut [*mut u8; N]) {
for p in ps.iter_mut() {
*p = rusty_alloc::alloc::malloc(SZ);
assert!(!p.is_null(), "child: malloc failed");
}
for i in (0..N).step_by(2) {
unsafe { rusty_alloc::alloc::free(ps[i]) };
}
}
fn drain_and_report(ps: &[*mut u8; N]) -> ! {
let mut reused = 0usize;
for _ in 0..N {
let q = rusty_alloc::alloc::malloc(SZ);
assert!(!q.is_null(), "child: malloc failed during drain");
if (0..N).step_by(2).any(|i| ps[i] == q) {
reused += 1;
}
}
println!("{SURVIVED} reused={reused}");
std::process::exit(0);
}
fn child_garbage_link() -> ! {
let mut ps = [core::ptr::null_mut::<u8>(); N];
build_poisonable_free_list(&mut ps);
for i in (0..N).step_by(2) {
unsafe { ps[i].cast::<usize>().write(0x4141_4141_4141_4140) };
}
drain_and_report(&ps);
}
fn child_aligned_out_of_segment_link() -> ! {
let mut ps = [core::ptr::null_mut::<u8>(); N];
build_poisonable_free_list(&mut ps);
for i in (0..N).step_by(2) {
unsafe {
let slot = ps[i].cast::<usize>();
slot.write(slot.read() ^ (1usize << 44));
}
}
drain_and_report(&ps);
}
fn assert_child_aborts(scenario: &str) {
let exe = std::env::current_exe().expect("current_exe");
let out = Command::new(exe)
.env(MARKER, scenario)
.arg("--test-threads=1")
.arg("--nocapture")
.output()
.expect("spawn child");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains(SURVIVED),
"[{scenario}] the child walked a POISONED free list to completion. \
Every corrupted link was accepted and handed out as an allocation — \
the free-list hijack primitive is live.\n{combined}"
);
assert!(
!out.status.success(),
"[{scenario}] the child exited cleanly after its free list was \
corrupted.\n{combined}"
);
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert_eq!(
out.status.signal(),
Some(SIGABRT),
"[{scenario}] the child died, but NOT on the corruption check. \
SIGSEGV here means the allocator decoded the attacker's bytes \
into a pointer and followed it — the mitigation did not fire, and \
'the child died' would otherwise have been read as a pass.\n\
{combined}"
);
}
}
fn mitigation_present() -> bool {
if cfg!(any(
feature = "secure",
feature = "linkcheck",
feature = "blockmap"
)) {
return true;
}
eprintln!(
"skipped: free-list encoding and the link check are `secure`-only, \
and this build does not enable it"
);
false
}
#[test]
#[cfg_attr(miri, ignore)] fn garbage_link_aborts() {
if let Ok(s) = std::env::var(MARKER) {
if s == "garbage" {
child_garbage_link();
}
return;
}
if !mitigation_present() {
return;
}
assert_child_aborts("garbage");
}
#[test]
#[cfg_attr(miri, ignore)] fn aligned_out_of_segment_link_aborts() {
if let Ok(s) = std::env::var(MARKER) {
if s == "aligned" {
child_aligned_out_of_segment_link();
}
return;
}
if !mitigation_present() {
return;
}
assert_child_aborts("aligned");
}