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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions