use std::{
fs::{create_dir_all, read_to_string},
os::unix::process::ExitStatusExt,
path::{Path, PathBuf},
process::{Command, Stdio},
};
const CONFIG_FILE: &str = env!("HARDENED_MALLOC_CONFIG_FILE");
const OUT_DIR: &str = env!("HARDENED_MALLOC_OUT_DIR");
const VENDOR_DIR: &str = env!("HARDENED_MALLOC_VENDOR_DIR");
const TEST_SOURCES: &[&str] = &[
"aligned_sized_delete_small_min_align",
"calloc_overflow",
"calloc_zeroed",
"double_free_large",
"double_free_large_delayed",
"double_free_small",
"double_free_small_delayed",
"free_sized_large",
"free_sized_small",
"impossibly_large_malloc",
"invalid_free_protected",
"invalid_free_small_region",
"invalid_free_small_region_far",
"invalid_free_unprotected",
"invalid_malloc_object_size_small",
"invalid_malloc_object_size_small_quarantine",
"invalid_malloc_usable_size_small",
"invalid_malloc_usable_size_small_quarantine",
"large_array_growth",
"malloc_info",
"malloc_noreuse",
"malloc_object_size",
"malloc_object_size_offset",
"malloc_object_size_zero",
"malloc_zero_different",
"offset",
"overflow_large_1_byte",
"overflow_large_8_byte",
"overflow_small_1_byte",
"overflow_small_8_byte",
"read_after_free_large",
"read_after_free_small",
"read_zero_size",
"realloc_init",
"string_overflow",
"unaligned_free_large",
"unaligned_free_small",
"unaligned_malloc_usable_size_small",
"uninitialized_free",
"uninitialized_malloc_usable_size",
"uninitialized_read_large",
"uninitialized_read_small",
"uninitialized_realloc",
"write_after_free_large",
"write_after_free_large_reuse",
"write_after_free_small",
"write_after_free_small_reuse",
"write_zero_size",
];
fn read_config(key: &str) -> Option<String> {
let content = read_to_string(CONFIG_FILE).ok()?;
for line in content.lines() {
let line = line.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 && parts[0].trim() == key {
return Some(parts[1].trim().to_string());
}
}
None
}
fn test_bin_dir() -> PathBuf {
PathBuf::from(OUT_DIR).join("test-bins")
}
fn compile_tests() {
let vendor_dir = Path::new(VENDOR_DIR);
let test_dir = vendor_dir.join("test");
let bin_dir = test_bin_dir();
let lib_path = PathBuf::from(OUT_DIR).join("libhardened_malloc.a");
create_dir_all(&bin_dir).unwrap();
let extended = read_config("CONFIG_EXTENDED_SIZE_CLASSES").unwrap_or("true".into());
let slab_canary = read_config("SLAB_CANARY").unwrap_or("true".into());
for name in TEST_SOURCES {
let src = test_dir.join(format!("{name}.c"));
let bin = bin_dir.join(name);
let status = Command::new("cc")
.arg("-std=c23")
.arg("-O0")
.arg("-D_GNU_SOURCE")
.arg(format!("-DSLAB_CANARY={slab_canary}"))
.arg(format!("-DCONFIG_EXTENDED_SIZE_CLASSES={extended}"))
.arg("-I")
.arg(&vendor_dir)
.arg("-I")
.arg(&vendor_dir.join("include"))
.arg("-I")
.arg(&test_dir)
.arg(&src)
.arg(&lib_path)
.arg("-lpthread")
.arg("-o")
.arg(&bin)
.status()
.unwrap_or_else(|error| panic!("failed to compile {name}: {error}"));
assert!(status.success(), "failed to compile {name}");
}
}
fn run_test(name: &str) -> i32 {
let bin = test_bin_dir().join(name);
let status = Command::new(&bin)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.unwrap_or_else(|error| panic!("failed to run {}: {error}", bin.display()));
match status.signal() {
Some(sig) => -(sig as i32),
None => status.code().unwrap_or(-1),
}
}
fn assert_aborts(name: &str) {
let code = run_test(name);
assert_eq!(code, -6, "{name}: expected SIGABRT (-6), got {code}");
}
fn assert_segfaults(name: &str) {
let code = run_test(name);
assert_eq!(code, -11, "{name}: expected SIGSEGV (-11), got {code}");
}
fn assert_success(name: &str) {
let code = run_test(name);
assert_eq!(code, 0, "{name}: expected success (0), got {code}");
}
#[test]
fn test_vendor() {
compile_tests();
assert_aborts("double_free_large");
assert_aborts("double_free_large_delayed");
assert_aborts("double_free_small");
assert_aborts("double_free_small_delayed");
assert_aborts("overflow_small_1_byte");
assert_aborts("overflow_small_8_byte");
assert_aborts("invalid_free_protected");
assert_aborts("invalid_free_small_region");
assert_aborts("invalid_free_small_region_far");
assert_aborts("invalid_free_unprotected");
assert_aborts("invalid_malloc_usable_size_small");
assert_aborts("invalid_malloc_usable_size_small_quarantine");
assert_aborts("invalid_malloc_object_size_small");
assert_aborts("invalid_malloc_object_size_small_quarantine");
assert_aborts("unaligned_free_large");
assert_aborts("unaligned_free_small");
assert_aborts("unaligned_malloc_usable_size_small");
assert_aborts("uninitialized_free");
assert_aborts("uninitialized_malloc_usable_size");
assert_aborts("uninitialized_realloc");
assert_aborts("write_after_free_small");
assert_aborts("write_after_free_small_reuse");
assert_segfaults("overflow_large_1_byte");
assert_segfaults("overflow_large_8_byte");
assert_segfaults("read_after_free_large");
assert_segfaults("read_zero_size");
assert_segfaults("write_after_free_large");
assert_segfaults("write_after_free_large_reuse");
assert_segfaults("write_zero_size");
assert_success("calloc_overflow");
assert_success("calloc_zeroed");
assert_success("free_sized_large");
assert_success("free_sized_small");
assert_success("impossibly_large_malloc");
assert_success("large_array_growth");
assert_success("malloc_object_size");
assert_success("malloc_object_size_offset");
assert_success("malloc_noreuse");
assert_success("realloc_init");
assert_success("uninitialized_read_small");
assert_success("uninitialized_read_large");
assert_success("malloc_info");
assert_success("read_after_free_small");
assert_success("string_overflow");
}