[Solved] Crash when using HashMap to track memory allocations

This question regards Rust 1.31.1 behavior on macOS.

I'm attempting to have an embedded C library (CPython in this case) use Rust's allocator. The details of this shouldn't be important. But essentially I implement some Rust functions that translate CPython's memory APIs (Memory Management — Python 3.10.6 documentation) to Rust's APIs.

Unlike C's and CPython's free(), Rust's dealloc() API requires the use of a std::alloc::Layout to specify how much memory to free (it doesn't just accept an address to release). This means that the Rust shim installed as Python's allocator needs to track metadata for each allocation so that it can call dealloc() appropriately.

My initial thinking was I would simply use a HashMap<*mut u8, std::alloc::Layout> to track allocations. This seems to "just work" for the first several allocations and frees. However, a few dozen allocations into the program, I get a crash when attempting to .insert() into this HashMap:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x800014480)
    frame #0: 0x000000010000dde6 pyapp`_$LT$std..collections..hash..table..Bucket$LT$K$C$$u20$V$C$$u20$M$GT$$GT$::peek::hcb391d544eabcd99(self=Bucket<*mut u8, core::alloc::Layout, &mut std::collections::hash::table::RawTable<*mut u8, core::alloc::Layout>> @ 0x00007ffeefbfec00) at table.rs:404
    frame #1: 0x000000010001a327 pyapp`std::collections::hash::map::search_hashed_nonempty::h817b3d1681f6a0d6(table=0x00007ffeefbff3a0, hash=(hash = 16411764986111601300), is_match=closure @ 0x00007ffeefbfeb80) at map.rs:455
    frame #2: 0x0000000100019053 pyapp`std::collections::hash::map::search_hashed::h968c430393186b91(table=0x00007ffeefbff3a0, hash=(hash = 16411764986111601300), is_match=closure @ 0x00007ffeefbfedd0) at map.rs:438
    frame #3: 0x000000010001e3fe pyapp`_$LT$std..collections..hash..map..HashMap$LT$K$C$$u20$V$C$$u20$S$GT$$GT$::insert_hashed_nocheck::h873f192e4db28fdb(self=0x00007ffeefbff390, hash=(hash = 16411764986111601300), k="", v=Layout @ 0x00007ffeefbfee68) at map.rs:992
    frame #4: 0x000000010001fcff pyapp`_$LT$std..collections..hash..map..HashMap$LT$K$C$$u20$V$C$$u20$S$GT$$GT$::insert::h61ae88444118f32f(self=0x00007ffeefbff390, k="", v=Layout @ 0x00007ffeefbff020) at map.rs:1372
  * frame #5: 0x000000010002ae3f pyapp`pyembed::pyalloc::raw_malloc::hd4322707fd376721(ctx=0x00007ffeefbff390, size=8224) at pyalloc.rs:34
    frame #6: 0x000000010054a36d pyapp`_PyObject_Malloc + 61

I'm quite surprised to see a crash in the bowels of HashMap.insert() - especially in a frame (peek()) that appears to be read-only!

I want to believe that I'm somehow corrupting the memory backing the HashMap. After all, I am mucking about with custom allocators. But unless my realloc() shim is misbehaving (I've audited it and believe it to be behaving correctly), I find it hard to believe that my allocator shim is touching memory backing Rust-allocated objects. Auditing allocation events shows mostly malloc() and free() calls with only ~4 realloc() very early during execution - and critically before dozens of other alloc() and free() calls which seem to work without issue.

Is it possible that HashMap's key hashing isn't compatible with *mut u8 keys or something like that?

My code is littered with a lot of unsafe {} (by necessity). A snapshot of that code can be found at https://gist.github.com/indygreg/2f8bbee226e67f80446214546b0d5c05. As far as I know everything is executing on the same OS thread.

Does anyone have a clue what could be causing this crash in HashMap? Does anyone have any tips for debugging issues in the Rust standard library like this?

While I'm relatively new to Rust, I am very computer literate. An eventual debugging step will likely be to reproduce on Linux so I can use rr to find where the HashMap's backing memory is getting poisoned. But I imagine this will be quite resource intensive. I'm hoping there's an "obvious" solution...

2 Likes

I posted a link to this issue on Twitter and the problem appears to be the lifetime of the HashMap. In the following function, allocs is dropped when it goes out of scope. The pointer still lingers and the data structure persists in memory and is still usable until a later operation claims its original memory.

pub fn make_raw_memory_allocator() -> pyffi::PyMemAllocatorEx {
    let allocs: HashMap<*mut u8, alloc::Layout> = HashMap::new();
    let ptr = &allocs as *const _;

    pyffi::PyMemAllocatorEx {
        ctx: ptr as *mut c_void,
        malloc: Some(raw_malloc),
        calloc: Some(raw_calloc),
        realloc: Some(raw_realloc),
        free: Some(raw_free),
    }
}

I worked around the problem by creating a struct to hold both the PyMemAllocatorEx and Box<HashMap> instances. This keeps the HashMap alive and causes the crashes to go away!

1 Like