Skip to content

Two independently-linked mimalloc v3 instances (CPython 3.14 vendored + static in a Python extension) SIGSEGV in _mi_theap_collect_retired at process exit when Arrow's v2 copy is also loaded (macOS arm64) #1327

Description

@kkollsga

On macOS arm64 a Python 3.14 process can end up hosting three independent mimalloc instances, and a specific pairing crashes at interpreter teardown:

  • CPython 3.14.3 vendors mimalloc v3.3.2 (initializes even in python -c pass)
  • our Rust extension (kglite, PyO3/abi3 cdylib) statically links mimalloc v3.3.1 as the Rust #[global_allocator] (via libmimalloc-sys, no override feature — we register no malloc zone and export no symbols beyond PyInit_*)
  • pyarrow 24.0.0's bundled libarrow carries its own mimalloc (v2-line, per MIMALLOC_VERBOSE banner)

Repro (deterministic, 100+ runs):

python3.14 -c "import pyarrow; import kglite; kglite.KnowledgeGraph()"
# exit 139 — SIGSEGV at teardown; both import orders; import alone suffices

Crash frame (lldb): EXC_BAD_ACCESS (address=0x17) in libarrow.2400.dylib at _mi_theap_collect_retired + 124, single-frame backtrace (fires from teardown machinery; unwind unavailable), register holding the page pointer contains 0x1 — a corrupted/foreign thread-heap page list. os._exit(0) avoids it entirely (teardown-only). Enabling faulthandler perturbs timing enough to sometimes mask it.

Discriminating matrix (all verified 5x, fresh venvs):

extension's mimalloc pyarrow result
v3.3.1 24.0.0 SIGSEGV
v3.3.1 23.0.1 / 22.0.0 clean
v2.3.01 (only change) 24.0.0 clean
none (allocator removed) 24.0.0 clean
v3.3.1 absent clean (CPython-v3 + ext-v3 alone coexist)

So the fatal combination requires two v3-line instances (CPython's + ours) plus pyarrow 24's presence — its v2 instance appears to change teardown ordering/registration such that one v3 instance's thread-heap cleanup walks state poisoned by the other. Not TLS-model dependent (local_dynamic_tls unchanged), not zone-related (no instance registers a zone in our builds).

Question for maintainers: are multiple independently-linked mimalloc copies in one process expected to be teardown-safe? If yes, this looks like a v3 regression in instance isolation (thread-done callbacks / reserved-slot sharing?); if no, that constraint deserves loud documentation — Rust wheels commonly ship #[global_allocator] mimalloc (v3 default since libmimalloc-sys 0.1.47), and CPython ≥3.13 vendors v3, so the collision surface is the whole PyO3 ecosystem × modern Python.

Our workaround: pinning our copy to the v2 line (mimalloc crate features = ["v2"]), shipping in kglite 0.12.12. Happy to provide the minimal reproducer wheel pair + full MIMALLOC_VERBOSE census logs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions