Skip to content

[Python][C++] SIGSEGV: NULL deref in bundled mimalloc mi_thread_init when first pyarrow import and first allocation happen on a short-lived thread #50908

Description

@kaushal9696

Components: Python, C++

Environment

  • pyarrow 25.0.0 (PyPI wheel, bundled mimalloc), pandas + pyarrow dataset/parquet path
  • Python 3.14.4, CPython, Linux x86_64 (Ubuntu-family, kernel 7.0.0), uvicorn/FastAPI service
  • Not OOM (VmRSS 161 MB at crash, process peak 130 MB), not disk.

Summary

If the first import of pyarrow and Arrow's first allocation happen on a short-lived worker thread that subsequently exits, a later allocation on a new thread crashes with SIGSEGV inside the bundled mimalloc's per-thread heap initialisation. When the import + first allocation happen on the main thread (or any thread that stays alive), the same workload is stable under heavy concurrency.

This bites any FastAPI/uvicorn-style service that lazily imports pandas/pyarrow inside a request handler: anyio's worker threads have a 10-second idle lifetime (WorkerThread.MAX_IDLE_TIME), so the import lands on a thread that is pruned seconds later, and the next burst of traffic on fresh threads segfaults the process.

Crash

From a core dump (gdb, thread apply all bt; crashing thread was the newest of 23):

#5  mi_thread_init ()                                        libarrow.so.2500
#6  _mi_malloc_generic ()
#8  arrow::BaseMemoryPoolImpl<MimallocAllocator>::Allocate(...)
#12 arrow::AllocateEmptyBitmap(...)
#14 parquet::SerializedFile::PreBuffer(...)
#45 pyarrow._dataset.Scanner.to_table
#49 _PyEval_EvalFrameDefault

Disassembly at the fault: mov (%rdx),%rax then => mov 0x18(%rax),%rax with rax == 0 — a NULL dereference inside mi_thread_init.

Isolation matrix (3 runs per configuration, deterministic)

pyarrow import first Arrow allocation outcome
throwaway thread (exits) same throwaway thread SEGV 3/3
main thread throwaway thread survived 3/3
throwaway thread main thread survived 3/3
main thread main thread survived 3/3

The trigger is thread identity at load/first-allocation time, not concurrency: with the import on the main thread, 8 concurrent fresh threads performed 6,000 parquet reads with no fault.

Reproduction shape

import threading

def load_and_read():
    import pyarrow.dataset as ds          # first pyarrow import in the process
    ds.dataset("some.parquet").to_table() # first Arrow allocation

t = threading.Thread(target=load_and_read)
t.start(); t.join()                       # thread exits — heap/TLS torn down

# later: allocations from NEW short-lived threads (e.g. anyio worker threads
# serving an ASGI app) crash in mi_thread_init.

End-to-end reproduction (our service shape): uvicorn app whose handler lazily imports pandas and reads parquet; hammer with 21 concurrent GETs per burst, 12 s idle between bursts (idle > anyio's 10 s worker lifetime so each burst gets fresh threads). Pre-workaround the process died with signal 11 on the third burst, reproducibly. Post-workaround: 10 bursts, 210 requests, all 200.

Hypothesis

mimalloc's process-wide initialisation appears to bind state to the thread that performed it; when that thread exits, its TLS/heap teardown leaves a structure NULL that mi_thread_init later dereferences on a new thread. (We can share the full backtrace and the isolation script on request.)

Workarounds (verified)

  • Import pyarrow and perform one real allocation on the main thread before any worker-thread use (we ship this as a startup warm-up).
  • ARROW_DEFAULT_MEMORY_POOL=system (set before import) also sidesteps it, at the cost of changing the allocator globally.

Reporter context: hit in production behind FastAPI/uvicorn; root-caused from an apport core dump; happy to provide the isolation script, full thread apply all bt, and library build info.

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