Skip to content

mi_free() crashes on foreign pointers in release MI_SECURE=OFF#1285

Open
jaschiu wants to merge 1 commit intomicrosoft:dev3from
jaschiu:segfault-fix
Open

mi_free() crashes on foreign pointers in release MI_SECURE=OFF#1285
jaschiu wants to merge 1 commit intomicrosoft:dev3from
jaschiu:segfault-fix

Conversation

@jaschiu
Copy link
Copy Markdown

@jaschiu jaschiu commented May 4, 2026

Hi, on my Gentoo Linux system where I have Librewolf (a fork of firefox) use mimalloc via LD_PRELOAD, I noticed that when running it with mimalloc secure mode off, it segfaults. I had my LLM vibecode this PR and the below description.

Summary

In a standard CMAKE_BUILD_TYPE=Release build with MI_SECURE=OFF (and no MI_DEBUG, non-Apple), mi_free(p) dereferences a NULL pagemap submap pointer and SIGSEGVs whenever p is a valid-but-foreign pointer — i.e. a pointer that was not allocated out of a mimalloc arena. Every other production allocator (glibc malloc, jemalloc, tcmalloc) tolerates such calls; mimalloc's own debug / MI_SECURE / __APPLE__ builds already do as well. Only the default release Linux fast path is affected.

This is observable in the wild: LD_PRELOAD=libmimalloc.so librewolf (LibreWolf 150.0.1, identical for Firefox with any Mesa-accelerated build) reliably segfaults during gbm_create_devicedlopen("libLLVM.so.20.1"), because an LLVM static initializer calls free() on a pointer that lives on a Mesa/llvmpipe worker thread's stack (or its pthread TCB page). I assume most Mesa-accelerated Firefox/Chromium setups on Linux hit this if they build mimalloc without MI_SECURE.

Repro (tested versions: v3.1.5, v3.2.8, v3.3.2, current dev3 tip (8e4230a) — all affected)

Environment:

  • Gentoo Linux, glibc 2.x, kernel 6.x
  • clang 21.1.8 + mold + LTO, CFLAGS=-march=native -O3 -flto ... (reproduces with plain -O2 -march=native too)
  • LibreWolf 150.0.1 (Firefox 150.0.1 derivative) with Mesa 25.3.6 / llvmpipe and libLLVM.so.20.1
  • 64-bit x86_64 (2-level pagemap, MI_PAGE_MAP_FLAT=0)

Steps:

cmake -S mimalloc -B build -DCMAKE_BUILD_TYPE=Release -DMI_SECURE=OFF -DMI_OPT_ARCH=OFF
cmake --build build -j
LD_PRELOAD=$PWD/build/libmimalloc.so.3 \
  timeout 15 librewolf -no-remote --profile $(mktemp -d) about:blank
# exit 139 (SIGSEGV) in under a second, every time

Backtrace (RelWithDebInfo)

Thread 1 "librewolf" received signal SIGSEGV, Segmentation fault.
#0 _mi_unchecked_ptr_page (p=0x7fffcf029320)  include/mimalloc/internal.h:644
#1 _mi_ptr_page            (p=0x7fffcf029320)  include/mimalloc/internal.h:663
#2 mi_validate_ptr_page    (p=0x7fffcf029320)  src/free.c:162
#3 mi_free_ex              (p=0x7fffcf029320)  src/free.c:170
#4 mi_free                 (p=0x7fffcf029320)  src/free.c:198
#5 libLLVM.so.20.1 (static initializer, offset 0x2f31000)
#6 libLLVM.so.20.1 (static initializer, offset 0x2f31301)
#7 _dl_init  (ld-linux-x86-64.so.2)
...
#20 gbm_create_device     (libgbm.so.1)
#21 libxul.so             (Gecko/Widget init)

Faulting instruction: mov (%rax,%rcx,8),%rdi with rax=0x0 (submap pointer), rcx=0xf02 (sub_idx) → SIGSEGV at si_addr = 0x7810. Matches the strace-visible signature si_addr=0x6650/0x7810/etc. (different sub_idx per run).

Pointer provenance

/proc/<pid>/maps shows 0x7fffcf029320 lying in a ~1008 KiB anonymous rw-p region bracketed by ---p guard pages — the canonical Mesa/llvmpipe worker-thread stack layout. It is NOT inside the mimalloc arena range (0x7fff85cd0000-0x7fffc5cd0000 in this run). Glibc's free() on this same pointer is a no-op (or a graceful "invalid pointer" error) — LibreWolf runs fine without LD_PRELOAD and with LD_PRELOAD=libmimalloc-secure.so.

Root cause

include/mimalloc/internal.h:658-664:

static inline mi_page_t* _mi_ptr_page(const void* p) {
  mi_assert_internal(p==NULL || mi_is_in_heap_region(p));
  #if MI_DEBUG || MI_SECURE || defined(__APPLE__)
  return _mi_checked_ptr_page(p);
  #else
  return _mi_unchecked_ptr_page(p);
  #endif
}

_mi_unchecked_ptr_page unconditionally dereferences _mi_page_map_at(idx):

static inline mi_page_t* _mi_unchecked_ptr_page(const void* p) {
  size_t sub_idx;
  const size_t idx = _mi_page_map_index(p, &sub_idx);
  return (_mi_page_map_at(idx))[sub_idx];  // NULL if p==NULL  <-- WRONG comment
}

The // NULL if p==NULL comment is incorrect — the load crashes on any p whose page-map submap slot is NULL, which happens for every pointer outside mimalloc's committed arena slices. mi_free_ex (src/free.c:170-171) is already written to tolerate page == NULL, and _mi_checked_ptr_page (include/mimalloc/internal.h:647-653) literally adds one if mi_unlikely(sub == NULL) return NULL;. The graceful contract exists — the release Linux fast path is the only call site that skips it.

Proposed fix (one hunk)

--- a/include/mimalloc/internal.h
+++ b/include/mimalloc/internal.h
@@ -657,11 +657,7 @@ static inline mi_page_t* _mi_checked_ptr_page(const void* p) {

 static inline mi_page_t* _mi_ptr_page(const void* p) {
   mi_assert_internal(p==NULL || mi_is_in_heap_region(p));
-  #if MI_DEBUG || MI_SECURE || defined(__APPLE__)
   return _mi_checked_ptr_page(p);
-  #else
-  return _mi_unchecked_ptr_page(p);
-  #endif
 }

Cost: one predicted-not-taken test/je per free. Benchmarked no measurable change in test-stress runtime on this box. Brings release Linux builds into behavioral parity with glibc / jemalloc / tcmalloc and with mimalloc's own MI_SECURE=ON / MI_DEBUG / __APPLE__ builds.

Regression test

Added test/test-free-foreign.c covering mi_free(NULL), mi_free(stack_ptr), mi_free(static_ptr), mi_free(arbitrary_va), and a roundtrip mi_mallocmi_free afterward. Registered via the existing foreach(TEST_NAME api api-fill stress free-foreign) loop in the top-level CMakeLists.txt. All 5 subtests pass with the fix; the stack_ptr/static_ptr/arbitrary_va subtests crash without it in the default release build.

Verification

On the repro machine (3 consecutive trials per row, timeout 12 librewolf -no-remote --profile <tmp> about:blank):

Build LibreWolf startup
No LD_PRELOAD 3/3 clean
v3.1.5 + MI_SECURE=OFF 3/3 SIGSEGV
v3.1.5 + MI_SECURE=ON 3/3 clean
v3.2.8 + MI_SECURE=OFF 3/3 SIGSEGV
v3.2.8 + MI_SECURE=ON 3/3 clean
v3.2.8 + MI_SECURE=OFF + the patch above 3/3 clean
v3.3.2 + MI_SECURE=OFF 3/3 SIGSEGV
v3.3.2 + MI_SECURE=OFF + the patch above 3/3 clean
dev3@8e4230a9 + MI_SECURE=OFF 3/3 SIGSEGV
dev3@8e4230a9 + MI_SECURE=OFF + the patch above 3/3 clean

The patch applies cleanly via git apply against v3.2.8, v3.3.2, and current dev3 tip; the hunk in _mi_ptr_page hasn't been touched upstream since v3.1.5. Full ctest suite (test-api, test-api-fill, test-stress, test-free-foreign, test-stress-dynamic) is 5/5 pass on both v3.3.2 and dev3 after the patch.

Why this wasn't caught earlier

  • Most mimalloc users with LD_PRELOAD use applications that don't dlopen LLVM/Mesa after mimalloc init (e.g. server workloads, benchmarks).
  • Gentoo's mimalloc-3.1.5 ebuild exposed a hardened USE flag that mapped to -DMI_SECURE=ON; Firefox/LibreWolf users with hardened set (a common Gentoo default) implicitly used the checked path and never hit this.
  • The mimalloc-3.2.8-r1 ebuild hard-codes -DMI_SECURE=OFF, newly exposing release users to the fast path.

Release Linux builds with MI_SECURE=OFF (and no MI_DEBUG, not __APPLE__)
used _mi_unchecked_ptr_page, which dereferences _mi_page_map_at(idx)
without a NULL check. Any `mi_free(p)` on a pointer outside mimalloc's
committed arena slices (e.g. a stack pointer or an allocation from a
library that wasn't intercepted by the override) therefore SIGSEGVs,
while glibc / jemalloc / tcmalloc / mimalloc-secure / mimalloc-debug /
mimalloc-__APPLE__ all handle such calls gracefully.

Observed via LD_PRELOAD=libmimalloc.so on LibreWolf 150 (Firefox
derivative) on Linux: `gbm_create_device` dlopens libLLVM.so.20.1 whose
static initializer calls `free()` on a Mesa/llvmpipe worker-thread
stack pointer.

Fix: route `_mi_ptr_page` through `_mi_checked_ptr_page` unconditionally.
The extra cost is one predicted-not-taken branch per free; stress tests
show no measurable regression. Adds a targeted `test-free-foreign`
regression test.
@jaschiu
Copy link
Copy Markdown
Author

jaschiu commented May 4, 2026

@jaschiu please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

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