rusty_alloc 0.3.2

Allocator core of the rusty_alloc pure-Rust remake of mimalloc v2.4.5: segments, free-list-sharded pages, lock-free cross-thread frees, first-class heaps, arenas and a mi_*-compatible surface. Detects double frees. MIT.
Documentation
//! M6/M7 gates: first-class heaps, arenas, subprocs, options, stats — one
//! deterministic single-test process (counters are exact here).

use rusty_alloc::init::{self, HeapBox};
use rusty_alloc::{alloc, arena, options};

unsafe fn hmalloc(hb: *mut HeapBox, n: usize) -> *mut u8 {
    // SAFETY: test owns the heap on this thread.
    unsafe { alloc::heap_malloc(hb, n) }
}

#[test]
fn heaps_arenas_subprocs_options() {
    // --- first-class heap: alloc, cross-heap free routing, delete-migrate ---
    let h1 = init::create_heap(7, false, -1);
    let mut blocks = Vec::new();
    // SAFETY: h1 is ours on this thread; blocks tracked and freed once.
    unsafe {
        for i in 0..500usize {
            let p = hmalloc(h1, 32 + (i % 900));
            assert!(!p.is_null());
            core::ptr::write_bytes(p, 0x77, 32 + (i % 900));
            blocks.push((p, 32 + (i % 900)));
        }
        // Frees route to the OWNING heap via xheap even though the thread's
        // default heap is different:
        for &(p, _) in blocks.iter().take(100) {
            alloc::free(p);
        }
        // Visitor: counts must match live blocks (400 remaining).
        let mut seen = 0usize;
        let ok = (*(*h1).heap.get()).visit_blocks(true, &mut |_area, block, _sz| {
            if !block.is_null() {
                seen += 1;
            }
            true
        });
        assert!(ok);
        assert_eq!(seen, 400, "visitor missed blocks");
        // contains/check_owned
        let (probe, _) = blocks[200];
        assert!(alloc::heap_contains_block(h1, probe));
        assert!(alloc::heap_check_owned(h1, probe));
        let foreign = alloc::malloc(64);
        assert!(!alloc::heap_contains_block(h1, foreign));
        alloc::free(foreign);
        // delete: contents survive migration to the backing heap.
        init::heap_delete(h1);
        for &(p, n) in blocks.iter().skip(100) {
            assert_eq!(p.read(), 0x77);
            assert_eq!(p.add(n - 1).read(), 0x77);
            alloc::free(p); // now owned by the backing heap
        }
    }

    // --- destroy: wholesale release, then the world still works ------------
    let h2 = init::create_heap(0, true, -1);
    // SAFETY: h2 ours; destroy drops the blocks per contract.
    unsafe {
        for i in 0..300usize {
            let p = hmalloc(h2, 64 + i * 7 % 2000);
            assert!(!p.is_null());
        }
        init::heap_destroy(h2);
    }
    let sanity = alloc::malloc(128);
    assert!(!sanity.is_null());
    // SAFETY: live block.
    unsafe { alloc::free(sanity) };

    // --- set_default / get_backing ----------------------------------------
    let h3 = init::create_heap(0, true, -1);
    // SAFETY: h3 ours.
    unsafe {
        let prev = init::set_default_heap(h3);
        let p = alloc::malloc(64); // lands in h3
        assert!(alloc::heap_contains_block(h3, p));
        alloc::free(p);
        init::set_default_heap(prev);
        assert_eq!(init::backing_heap(), prev);
        init::heap_destroy(h3);
    }

    // --- arenas: exclusive reserve + heap_new_in_arena ---------------------
    let arena_id = arena::reserve_os_memory_ex(64 * 1024 * 1024, true, false, true)
        .expect("arena reserve failed");
    let (abase, asize) = arena::arena_area(arena_id);
    assert!(!abase.is_null() && asize == 64 * 1024 * 1024);
    let ha = init::create_heap(0, true, arena_id);
    // SAFETY: ha ours.
    unsafe {
        let s0 = (*(*ha).heap.get()).stats.segments;
        let p = hmalloc(ha, 100_000);
        assert!(!p.is_null());
        let addr = p.addr();
        assert!(
            addr >= abase.addr() && addr < abase.addr() + asize,
            "exclusive-arena heap allocated outside its arena"
        );
        assert!((*(*ha).heap.get()).stats.segments > s0);
        alloc::free(p);
        init::heap_destroy(ha);
    }
    // Chunk recycling: a second arena heap reuses the freed chunk.
    let hb2 = init::create_heap(0, true, arena_id);
    // SAFETY: hb2 ours.
    unsafe {
        let p = hmalloc(hb2, 4096);
        assert!(!p.is_null());
        assert!(p.addr() >= abase.addr() && p.addr() < abase.addr() + asize);
        // zalloc over a recycled (dirty) arena chunk must be re-zeroed:
        let z = alloc::heap_zalloc(hb2, 200_000);
        for i in (0..200_000).step_by(4096) {
            assert_eq!(z.add(i).read(), 0, "dirty arena chunk leaked at +{i}");
        }
        alloc::free(z);
        alloc::free(p);
        init::heap_destroy(hb2);
    }

    // --- subprocs: isolation of abandonment --------------------------------
    // (native only: the miri mock's TLS destructors never fire, so thread
    // exit does not abandon — documented prim/mock limitation)
    if cfg!(miri) {
        return;
    }
    let sp = init::subproc_new();
    let handle = std::thread::spawn(move || {
        init::subproc_add_current_thread(sp);
        let p = alloc::malloc(5000);
        assert!(!p.is_null());
        // SAFETY: leak intentionally; thread exit abandons into subproc sp.
        unsafe { core::ptr::write_bytes(p, 0x3C, 5000) };
        p as usize
    });
    let leaked = handle.join().unwrap();
    // Main (subproc 0) reclaim must NOT see sp's segments:
    let before = alloc::stats().reclaims;
    let churn: Vec<Vec<u8>> = (0..100).map(|_| vec![1u8; 40_000]).collect();
    drop(churn);
    let _ = before; // reclaims may move for other reasons; the real check:
    let mut found = 0usize;
    init::abandoned_visit_blocks(sp, -1, true, &mut |_a, b, _s| {
        if !b.is_null() {
            found += 1;
        }
        true
    });
    assert!(found >= 1, "abandoned block not visible in its subproc");
    // SAFETY: leaked block is still live (abandoned, not freed).
    unsafe { alloc::free(leaked as *mut u8) };

    // --- options + stats smoke ---------------------------------------------
    // v1 default: purging is opt-in (-1) pending the M8 open defect.
    assert_eq!(options::get(15), -1, "purge_delay default");
    options::set(15, 0);
    assert_eq!(options::get(15), 0);
    assert!(
        options::get_size(23) >= 1024 * 1024 * 1024,
        "arena_reserve KiB scaling"
    );
    let merged = rusty_alloc::stats::merged();
    assert!(merged.allocs > 0);
    let (_, _, _, rss, ..) = rusty_alloc::stats::process_info();
    assert!(rss > 0, "process_info rss");
}