Skip to content

Fix realloc crash when realloc is the first allocation on a thread (#1304)#1312

Open
gbaraldi wants to merge 1 commit into
microsoft:dev3from
gbaraldi:fix/realloc-null-theap-1304
Open

Fix realloc crash when realloc is the first allocation on a thread (#1304)#1312
gbaraldi wants to merge 1 commit into
microsoft:dev3from
gbaraldi:fix/realloc-null-theap-1304

Conversation

@gbaraldi

@gbaraldi gbaraldi commented Jun 18, 2026

Copy link
Copy Markdown

Found this while running Julia with mimalloc 3. Seems like just a missing guard.

AI Slop ahead

Summary

Fixes #1304 — a crash when realloc is the first mimalloc call on a freshly created thread.

On platforms with a fixed or dynamic TLS slot (macOS, Windows, OpenBSD), the thread-local default theap is NULL until the first allocation lazily initializes it (MI_THEAP_INITASNULL). Every malloc path tolerates a NULL theap and routes to the generic lazy-init path, but the in-place fast path of _mi_theap_realloc_zero evaluated _mi_theap_heap(theap) unconditionally:

if mi_unlikely(newsize<=size && newsize>=(size/2) && newsize>0
               && mi_page_heap(page)==_mi_theap_heap(theap))   // derefs theap->heap

_mi_theap_heap() does mi_atomic_load_acquire(&theap->heap), so when a fresh thread's first call is an in-place-fitting mi_realloc of a block allocated on another thread, theap == NULL and this dereferences a null pointer → SIGSEGV. (mi_malloc does not hit this because all malloc paths already guard theap != NULL under MI_THEAP_INITASNULL.)

Reproduction

First allocator call on a brand-new thread is an in-place-fitting realloc of a block from another thread:

void* p = mi_malloc(64);                 // main thread
// ... on a fresh worker thread, with no prior mimalloc call:
void* q = mi_realloc(p, 48);             // 48 in [32,64] -> in-place candidate -> deref NULL theap

lldb on macOS arm64 (v3.3.2): EXC_BAD_ACCESS (code=1, address=0x8) in _mi_theap_realloc_zero, instruction ldapr x8, [x8] with x8 == 0 — i.e. the acquire-load of theap->heap (offset 0x8) through a NULL theap. This matches the crash frame reported in #1304 (libcurl/libopenssl + many std::async workers).

Fix

Guard the in-place check with mi_theap_is_initialized(theap) under #if MI_THEAP_INITASNULL:

  if mi_unlikely(newsize<=size && newsize>=(size/2) && newsize>0
                  #if MI_THEAP_INITASNULL
                  && mi_theap_is_initialized(theap)
                  #endif
                  && mi_page_heap(page)==_mi_theap_heap(theap))

A NULL (or partially-initialized) theap then falls through to mi_theap_umalloc, which lazily initializes the thread heap, copies, and frees the old block — exactly as mi_malloc already does. This is also semantically correct: if the thread has no heap yet, p necessarily belongs to a different heap, so the "same heap" in-place check would be false regardless.

mi_theap_is_initialized (rather than a bare theap != NULL) additionally covers the theap != NULL && heap == NULL partial-init window. The guard compiles out entirely on the THREAD_LOCAL model (Linux), where the default theap is never NULL, so there is no added cost on that hot path.

Test

Adds test/test-realloc-thread.c (registered under MI_BUILD_TESTS): an in-place-fitting and a growing realloc as the first allocation on a fresh thread. It SIGSEGVs without this patch (exit 139) and passes with it.

Verified on macOS arm64 (MI_TLS_MODEL_FIXED_SLOT): test-realloc-thread 4/4, test-api 37/0, test-api-fill 17/0, test-stress clean.

🤖 Generated with Claude Code

…ft#1304)

On platforms with a fixed or dynamic TLS slot (macOS, Windows, OpenBSD) the
thread-local default theap is NULL until the first allocation lazily
initializes it (MI_THEAP_INITASNULL). All malloc paths tolerate a NULL theap,
but the in-place fast path of _mi_theap_realloc_zero evaluated
_mi_theap_heap(theap) unconditionally, dereferencing the NULL theap and
crashing (SIGSEGV at offset 0x8) when a fresh thread's first mimalloc call was
an in-place-fitting mi_realloc of a block allocated on another thread.

Guard the in-place check with mi_theap_is_initialized(theap) under
MI_THEAP_INITASNULL, so a NULL (or partially-initialized) theap falls through
to mi_theap_umalloc, which lazily initializes the thread heap and copies. The
guard compiles out on the THREAD_LOCAL model (Linux), where the default theap
is never NULL, so there is no extra hot-path cost there.

Add test/test-realloc-thread.c covering an in-place-fitting and a growing
realloc as the first allocation on a fresh thread (SIGSEGV before this fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gbaraldi

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="JuliaHub"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant