Skip to content

fix: avoid deadlock when growing dynamic thread-local slots #1358

Description

Symptom

On current dev3 (f45596ab), a full-debug build hangs deterministically when one thread creates enough heaps to grow the dynamic thread-local slot array beyond its initial capacity.

This is reachable through public APIs only:

#include <mimalloc.h>

int main(void) {
  mi_heap_t* heaps[40];
  for (size_t i = 0; i < 40; i++) {
    heaps[i] = mi_heap_new();
    if (heaps[i] == NULL) return 1;
    if (mi_heap_malloc(heaps[i], 32) == NULL) return 2;
  }
  for (size_t i = 0; i < 40; i++) mi_heap_destroy(heaps[i]);
  return 0;
}

Configuration and result on stock upstream:

cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DMI_DEBUG_FULL=ON \
  -DMI_OVERRIDE=OFF \
  -DMI_BUILD_SHARED=OFF \
  -DMI_BUILD_STATIC=ON
cmake --build build --target mimalloc-test-threadlocal-growth
timeout 10s build/mimalloc-test-threadlocal-growth

exit 124 (deterministic timeout)

Tested with GCC 14.2.1 on Linux. The same focused test also hangs with MinGW-w64 GCC 14.2.0.

Cause

The dynamic slot array was moved to meta allocation in ac61f0fb. Its growth path now forms a same-thread lock cycle:

mi_thread_locals_expand
  -> _mi_meta_rezalloc
     -> acquire subproc->theap_meta_lock
     -> mi_theap_rezalloc
        -> moving realloc calls mi_free(oldp)
           -> mi_stat_free recognizes a meta page
              -> acquire subproc->theap_meta_lock again

theap_meta_lock is not recursive, so the second acquisition never completes. An attached debugger stops in __lll_lock_wait.

The move is guaranteed once the slot array crosses a size class, so this is not timing-dependent. Applications that create enough independent heaps can make assertion/statistics-enabled allocator builds stop making progress.

Proposed fix

The slot array has a simpler lifetime rule than a general realloc needs:

  1. allocate a zeroed replacement from the meta allocator while it holds its lock;
  2. copy only the old logical header and slots;
  3. publish the replacement to thread-local storage;
  4. free the old meta block after the allocation lock has been released.

This removes the nested lock acquisition and explicitly guarantees that newly added slots are zero. _mi_meta_rezalloc has no other callers and can be removed.

I have a focused regression test that is RED by deterministic timeout before the change and exits successfully afterward. I will send the patch as a separate PR referencing this issue.

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