hardened-malloc-sys 16.0.2026060603

Rust bindings for GrapheneOS allocator
Documentation
//
// hardened-malloc-sys: Rust bindings for GrapheneOS allocator
// tests/vendor.rs: Integration tests for vendored hardened-malloc test suite
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: MIT

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");

// Vendored C test programs to compile and run.
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();

    // Tests that expect SIGABRT (fatal allocator error).
    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");

    // Tests that expect SIGSEGV.
    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");

    // Tests that expect success (exit 0).
    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");

    // Upstream tests with inverted exit code logic, see commit 588db29.
    // Skipped: malloc_object_size_zero, malloc_zero_different.
}