You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two frame-identical SIGSEGVs (EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x18) in mimalloc v3.3.1 statically linked into Apache Arrow C++ 25.0.0 (libarrow.2500.dylib, MI_OVERRIDE=OFF, MI_BUILD_SHARED=OFF), on macOS 26.5.1 arm64.
The crash is a NULL mi_theap_t::tld dereferenced by _mi_heap_main(), reached from the final
statistics increment in mi_thread_init(). It is intermittent and load-dependent: it fires early in
a process that creates many short-lived threads, and the same workload later completed 32,091
operations untouched.
Environment
mimalloc
v3.3.1 (as pinned by apache/arrowcpp/thirdparty/versions.txt)
build flags
MI_OVERRIDE=OFF, MI_BUILD_SHARED=OFF, MI_BUILD_OBJECT=OFF, MI_LOCAL_DYNAMIC_TLS=ON, MI_NO_OPT_ARCH=ON; static lib linked into a dylib
host
macOS 26.5.1 (25F80), arm64 (Apple Silicon)
consumer
CPython 3.12.13, pyarrow 25.0.0; a Python ETL writing Delta tables via delta-rs
threads at crash
93 and 104 respectively (two tokio runtimes, a polars pool, ~32 Python worker threads)
Only one mimalloc instance is present in the process (verified by binary inspection: the other
native extensions bundle jemalloc or use system malloc; CPython 3.12 vendors no mimalloc). This is not a multiple-instance teardown problem.
Stale empty-sentinel inherited via a recycled thread control block._mi_thread_done() sets
the slots to &_mi_theap_empty rather than NULL, and mimalloc writes slot 108 directly (no pthread_key_create), so a recycled TCB inheriting the sentinel looked like an excellent
explanation for "non-NULL theap, NULL tld". Falsified: reading the shipped binary, __mi_theap_empty (in __DATA_CONST,__const) has .tld = <non-NULL static>, .heap = 0x0, .refcount = 1. A stale sentinel would give a non-NULL tld.
Unsynchronized mi_heap_main_init() double-checked init._mi_process_attach is a load-time
initializer in this build (tail-calls _mi_auto_process_init, and _mi_process_init is
once-guarded), so process/main-heap init completes single-threaded at dylib load.
there is no NULL check between those two lines, whereas the other _mi_theap_create() call site (in heap.c) does check its result. If mi_tld_alloc() can return NULL — or return a tld that is not
yet published — a theap with tld == NULL is installed into the TSD slot, and the very next
statement in mi_thread_init() dereferences it. That would match the fault exactly. The
meta-page allocator backing mi_tld_alloc() is relatively young in the 3.3.x line, which is why I
raise it rather than assert it.
Question for maintainers: is mi_tld_alloc() allowed to fail or to return an unpublished tld here, and should _mi_heap_main()'s guard cover a non-NULL theap with a NULL tld?
Reproduction
No minimal reproducer — the crash is non-deterministic and appeared twice in ~12 minutes under a
workload that creates a new short-lived thread per buffered flush, each of which makes its first
mimalloc allocation while ~100 threads are live. Identical subsequent runs completed 32,091 and
1,789 operations without crashing. Shape needed to reproduce: many short-lived threads, each
touching mimalloc for the first time, in a process with a high live-thread count.
I have two full .ips crash reports and can attach redacted copies or answer specific questions against them.
Workaround in use
Selecting Arrow's system allocator (ARROW_DEFAULT_MEMORY_POOL=system) removes mimalloc from the
allocation path entirely; verified by driving the same ExportSchema chain and confirming the
per-backend byte counters move to the system pool.
Summary
Two frame-identical
SIGSEGVs (EXC_BAD_ACCESS,KERN_INVALID_ADDRESS at 0x18) in mimallocv3.3.1 statically linked into Apache Arrow C++ 25.0.0 (
libarrow.2500.dylib,MI_OVERRIDE=OFF,MI_BUILD_SHARED=OFF), on macOS 26.5.1 arm64.The crash is a NULL
mi_theap_t::tlddereferenced by_mi_heap_main(), reached from the finalstatistics increment in
mi_thread_init(). It is intermittent and load-dependent: it fires early ina process that creates many short-lived threads, and the same workload later completed 32,091
operations untouched.
Environment
apache/arrowcpp/thirdparty/versions.txt)MI_OVERRIDE=OFF,MI_BUILD_SHARED=OFF,MI_BUILD_OBJECT=OFF,MI_LOCAL_DYNAMIC_TLS=ON,MI_NO_OPT_ARCH=ON; static lib linked into a dylibOnly one mimalloc instance is present in the process (verified by binary inspection: the other
native extensions bundle jemalloc or use system malloc; CPython 3.12 vendors no mimalloc). This is
not a multiple-instance teardown problem.
Faulting stack (identical in both crashes)
The faulting thread is a plain Python worker thread, and
mi_thread_initin the stack meansthis was that thread's first mimalloc allocation.
Instruction-level analysis
Disassembling the faulting PC (
_mi_heap_main):include/mimalloc/types.hat v3.3.1:mi_theap_s.tldat 0,mi_tld_s.subprocat 0x18.x9 = 0x0, fault address0x18,vmRegionInfo:"0x18 is not in any region."
mi_thread_init()—mi_heap_stat_increase(mi_heap_main(), threads, 1).So: the thread's
theappointer in TSD slot 108 was non-NULL (passing the only guard in_mi_heap_main) while itstldwas NULL.Hypotheses I ruled out
what the disassembly shows in use. Also a different signature (SIGTRAP at thread exit).
every loaded native library.
_mi_thread_done()setsthe slots to
&_mi_theap_emptyrather than NULL, and mimalloc writes slot 108 directly (nopthread_key_create), so a recycled TCB inheriting the sentinel looked like an excellentexplanation for "non-NULL theap, NULL tld". Falsified: reading the shipped binary,
__mi_theap_empty(in__DATA_CONST,__const) has.tld = <non-NULL static>,.heap = 0x0,.refcount = 1. A stale sentinel would give a non-NULLtld.mi_heap_main_init()double-checked init._mi_process_attachis a load-timeinitializer in this build (tail-calls
_mi_auto_process_init, and_mi_process_initisonce-guarded), so process/main-heap init completes single-threaded at dylib load.
Remaining hypothesis (offered, not proven)
In
_mi_thread_init_theap_default():there is no NULL check between those two lines, whereas the other
_mi_theap_create()call site (inheap.c) does check its result. Ifmi_tld_alloc()can return NULL — or return atldthat is notyet published — a theap with
tld == NULLis installed into the TSD slot, and the very nextstatement in
mi_thread_init()dereferences it. That would match the fault exactly. Themeta-page allocator backing
mi_tld_alloc()is relatively young in the 3.3.x line, which is why Iraise it rather than assert it.
Question for maintainers: is
mi_tld_alloc()allowed to fail or to return an unpublishedtldhere, and should_mi_heap_main()'s guard cover a non-NULL theap with a NULLtld?Reproduction
No minimal reproducer — the crash is non-deterministic and appeared twice in ~12 minutes under a
workload that creates a new short-lived thread per buffered flush, each of which makes its first
mimalloc allocation while ~100 threads are live. Identical subsequent runs completed 32,091 and
1,789 operations without crashing. Shape needed to reproduce: many short-lived threads, each
touching mimalloc for the first time, in a process with a high live-thread count.
I have two full
.ipscrash reports and can attach redacted copies or answer specific questions against them.Workaround in use
Selecting Arrow's system allocator (
ARROW_DEFAULT_MEMORY_POOL=system) removes mimalloc from theallocation path entirely; verified by driving the same
ExportSchemachain and confirming theper-backend byte counters move to the system pool.