page-map: free(NULL) before init must not fault (glibc 2.44 startup segfault) - #30
Merged
Merged
Conversation
…ree(NULL) works before init glibc 2.44's __newlocale calls free(NULL) from the loader, before any constructor of the executable has run. With malloc overridden that lands in mi_free, whose release-mode lookup (_mi_unchecked_ptr_page) reads submaps[0][0] without checking the submap. The initial static page map (mi_page_map_empty, the microsoft#1341 fix) carried submaps[0] = NULL since the 2-level page-map restructure (upstream dev3 d63979a), so the process faulted at address 0 before main(). Point submaps[0] at a static zero-initialized submap instead. Every lookup through the empty map now yields no page, and mi_free returns. Adds test-free-before-init, which makes the call from .preinit_array (the same point in startup as the glibc call) and segfaulted without the fix.
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Aug 25, 2026
…s (glibc 2.44 startup crash) (#40409) ### What does this PR do? Bumps mimalloc to `942b8342` on oven-sh/mimalloc `bun-dev3-v2`, which merges oven-sh/mimalloc#30. `mi_free(NULL)` before mimalloc has initialized faults at address 0. The static initial page map (`mi_page_map_empty`, upstream's fix for microsoft/mimalloc#1341) has carried `submaps[0] = NULL` since upstream dev3's 2-level page-map restructure (`d63979ae`), and the release-mode lookup (`_mi_unchecked_ptr_page`) reads `submaps[0][0]` without checking the submap: ``` free+0x28: movq (%rax,%rcx), %rdi ; rax = pmap->submaps[0] = NULL ``` The Aug 13 fork sync (#37367) picked that up; upstream dev3 still has the bug. The fix points `submaps[0]` at a static zero-initialized submap (64 KiB of `.bss`, untouched unless something frees before init), so the lookup yields no page and `mi_free` returns. Only the pin moves here. **How this gets hit:** glibc 2.44 refactored `__newlocale` into a wrapper that ends with an unconditional `free(tmp_buffer)` (a leak fix for the `LOCPATH` buffer), so every `newlocale(LC_ALL, "C", NULL)` is now one `free(NULL)`. libstdc++'s iostream initializer is `init_priority(90)` and calls exactly that through `std::locale()`, and 90 sorts before mimalloc's `constructor(101)` — so on glibc 2.44 (Arch, CachyOS, Fedora rawhide) any binary that exports the override crashes in `.init_array`, before `main()`. That's also the `nix`-on-Arch crash in microsoft/mimalloc#1341. **Does this affect oven-sh/bun binaries?** Not today: `linker.lds` keeps the Linux `malloc`/`free` override private to the executable, so libc's internal `free` still goes to glibc's allocator. It bites any build that exports the override; taking it here so the fork doesn't carry a pin divergence and so the next person to export it doesn't rediscover this. ### How did you verify your code works? In oven-sh/mimalloc#30: a new `test-free-before-init` calls `free(NULL)`/`mi_free(NULL)` from `.preinit_array` — before any constructor. Linux (Ubuntu 24.04 arm64, gcc, `MI_OVERRIDE=ON`): segfaults (exit 139) without the fix, passes with it; Release `ctest` 23/23 and `MI_DEBUG_FULL` green. Also checked on a Linux build that exports the override, with an `LD_PRELOAD` library whose constructor calls `free(NULL)` (volatile pointer so gcc can't elide it; `dladdr` confirms it binds to the executable's `free`): old pin → SIGSEGV, this pin → `bun --version` prints normally. Not built end-to-end here — CI is the build; nothing else in the pin delta.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Claude Code 2.1.243 linux-x64 startup segfault (anthropics/claude-code#89389 and ~12 duplicates):
claude --versiondies with SIGSEGV infree → __newlocale → pthread_once, beforemain(), on glibc 2.44 (Arch, CachyOS).What happens
glibc 2.44's
__newlocalecallsfree(NULL)from the dynamic loader, before any constructor has run. Bun overridesmallocon Linux, so that lands inmi_free, whose release-mode lookup (_mi_unchecked_ptr_page) readssubmaps[0][0]without checking the submap:The initial static page map (
mi_page_map_empty, the upstream microsoft#1341 fix) has carriedsubmaps[0] = NULLsince the 2-level page-map restructure (upstream dev3d63979ae, Aug 9). The Aug 13 fork sync picked that up, bun-internal 1021 shipped it in CC 2.1.242/243, and 1038 (a178e44a) has byte-identicalfreecode — so the pending CC bump carries the crash forward.Fix
Point
submaps[0]at a static zero-initialized submap (64 KiB of.bss, never touched unless something frees before init). Every lookup through the empty map yields no page, andmi_freereturns.Test
test-free-before-initmakes the call from.preinit_array, which the loader runs before every.init_arrayentry — the same point in startup as the glibc call. On Linux (Ubuntu 24.04 arm64, gcc,-DMI_OVERRIDE=ON):Segmentation fault, exit 139ctest23/23 green in Release, and the test +test-apipass underMI_DEBUG_FULLUpstream dev3 has the same bug; I'll send it there separately.