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.
On macOS arm64 a Python 3.14 process can end up hosting three independent mimalloc instances, and a specific pairing crashes at interpreter teardown:
python -c pass)kglite, PyO3/abi3 cdylib) statically links mimalloc v3.3.1 as the Rust#[global_allocator](via libmimalloc-sys, nooverridefeature — we register no malloc zone and export no symbols beyondPyInit_*)pyarrow24.0.0's bundled libarrow carries its own mimalloc (v2-line, per MIMALLOC_VERBOSE banner)Repro (deterministic, 100+ runs):
Crash frame (lldb):
EXC_BAD_ACCESS (address=0x17)inlibarrow.2400.dylibat_mi_theap_collect_retired + 124, single-frame backtrace (fires from teardown machinery; unwind unavailable), register holding the page pointer contains0x1— 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):
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_tlsunchanged), 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.