Repository navigation
fix(mem): vendor mimalloc 3.5.3 so free(NULL) before init is safe (supersedes #2562) - #2731
Conversation
With mimalloc 3.3.2's release build, free(NULL) before the allocator's constructor dereferenced a page map that does not exist yet. glibc 2.44's newlocale() does exactly that during libstdc++ start-up, so every Linux binary died with SIGSEGV before main (reported with a reproduction in #2562). Upstream fixed it in b2226c19 (mimalloc #1341), first released in v3.4.4; v3.5.3, upstream's latest published release, contains it. Vendored from microsoft/mimalloc tag v3.5.3 (tag object deac3dcf, commit d4881d338125e1cb7c47ba4cfb398d6f7c0c8d45): LICENSE, include/ and src/, written blob by blob from a bare clone. Before the local patches were reapplied, the staged LICENSE blob and include/ and src/ trees had the tag's exact object ids (50 files; src/arena-meta.c is gone upstream). Afterwards the only differences are two documented hunks: - src/prim/unix/prim.c: macOS always recommits on decommit, carried from 63fb7b3 (the phys_footprint fix). - src/prim/osx/alloc-override-zone.c: one comment reworded, because scripts/security-vendored.sh rejects the upstream "fork()" spelling even inside a comment. No code change. The reproducible-banner patch (no __DATE__/__TIME__) is dropped: upstream removed the build clock in v3.4.5 (mimalloc #1355). The two Makefile comments that pointed at that patch now say so. The production object is built with -DMI_PROFILE=0: mimalloc 3.5.2+ compiles its allocation-sampling profiler in by default, with a sample countdown on the allocation path, and the product never uses it. vendored/mimalloc/PATCHES.md records the pin, a re-verification recipe, the check that pins each patch and the refresh procedure; THIRD_PARTY.md points at it, the SBOM version is 3.5.3 and scripts/vendored-checksums.txt is regenerated (mimalloc lines only; security-vendored.sh passes). Measured on Apple M5 Pro (macOS 26.6), full indexes, 2 interleaved reps, main fceb25d (3.3.2) -> this change: TypeScript peak phys_footprint 1.07/1.01 -> 1.04/1.04 GB, peak RSS 1.87/1.84 -> 1.85/1.86 GB, wall 30.6/29.1 -> 29.3/30.0 s; django footprint 0.41/0.41 -> 0.38/0.39 GB, RSS 0.59/0.59 -> 0.58/0.60 GB, wall 6.5/6.2 -> 6.5/6.5 s. The graphs are identical: full node and edge hashes, node and edge census, no dangling edges (django 68022 nodes / 322464 edges, TypeScript 306239 / 835984). The same tree without the recommit patch peaks at 2.09/2.04 GB footprint on TypeScript, so 63fb7b3 still does its job. Linux arm64 (glibc 2.39, gcc 13.3): both trees build without warnings; django peak RSS 0.67/0.68 GB on main, 0.69/0.68 GB with this change. Co-authored-by: Felix Rath <[email protected]> Signed-off-by: Martin Vogel <[email protected]>
The unit suites link mimalloc's debug build without the global override, so they could not see the #2562 start-up crash, and nothing guarded the vendored allocator's local behaviour: prod_mimalloc.o is compiled with -w and without CFLAGS_COMMON, so -Wdate-time never reaches the vendored sources, and the macOS recommit patch had no test at all. tests/test_prod_allocator_contract.sh links three small fixtures against $BUILD_DIR/prod_mimalloc.o itself: 1. free(NULL) from an ELF .preinit_array hook, before every constructor (Linux). Also asserts malloc returns a mimalloc-owned block, so a fixture that did not link the override fails instead of passing. 2. mi_options_print_out's first line is exactly "vX.Y.Z" with no build clock, and the object is the release build (debug level 0). Upstream dropped the clock in v3.4.5; this keeps a refresh from bringing it back. The banner also has no "profiling" line, which pins -DMI_PROFILE=0. 3. _mi_prim_decommit sets needs_recommit on macOS and leaves upstream's release value elsewhere (local patch 1 in vendored/mimalloc/PATCHES.md). scripts/test.sh runs it right after Step 5 builds the production binary, on every Linux and macOS leg; other platforms skip with the reason. Linux arm64 (glibc 2.39), gcc 13.3 and clang 18.1, 4 runs each: main's 3.3.2 object segfaults in fixture 1 (exit 139) 4/4 with both compilers; this branch passes all three fixtures 4/4 with both. macOS arm64: reverting the recommit patch turns fixture 3 red (needs_recommit=0); an object with a git-describe suffix in the banner, built with MI_DEBUG=2, or built with MI_PROFILE=1 turns fixture 2 red. The preinit reproduction follows the test Felix Rath wrote for #2562. Co-authored-by: Felix Rath <[email protected]> Signed-off-by: Martin Vogel <[email protected]>
bc51fd1 to
ddc986e
Compare
mimalloc 3.5.3 treats a MinGW build as having a process-attach hook only when MI_MINGW_UCRT64 is defined (src/prim/windows/prim.c). Upstream's CMake sets it; Makefile.cbm did not. src/prim/prim.c therefore also compiled its constructor, and _mi_auto_process_init ran twice on Windows: once from that constructor and once from the CRT TLS callback. The second run flushes the delayed-output buffer to stderr again, and the first flush had left a newline in it, so every Windows run started with a blank line on stderr. That is what failed smoke-win's one-line --version check. In the MI_DEBUG=2 test build the same second run reaches mi_assert_internal(mi_out_default == NULL) in options.c, which aborts. 3.3.2 had no such condition. Define it for IS_MINGW builds in both the production and the test mimalloc flags. Every MinGW toolchain this repository builds with (MSYS2 CLANG64 and CLANGARM64) is UCRT. No vendored source changes. Signed-off-by: Martin Vogel <[email protected]>
The MSan lane compiles mimalloc without -fsanitize=memory on purpose, so MSan never sees mimalloc write its own stack. Its text output is the one place those stack bytes reach an MSan interceptor: a message formatted in a mimalloc stack buffer arrives at fputs still carrying the shadow an earlier instrumented frame left there (cbm_log's line_buf), and MSan reports initialized bytes as uninitialized. mimalloc 3.5.3 prints an arena-alignment fallback warning on the x86-64 MSan lane, which turned this into a use-of-uninitialized-value report in the mem suite (_mi_prim_out_stderr, 43 bytes: the per-thread prefix). Under __has_feature(memory_sanitizer) only, the test runner now registers a mimalloc output handler at the top of main that unpoisons the finished text and writes it to stderr. Registering replays mimalloc's delayed-output buffer, which its constructor already wrote to stderr before main, so the handler skips that replay instead of repeating it behind a stray newline. Production code and every non-MSan build are unchanged. Signed-off-by: Martin Vogel <[email protected]>
|
Pushed two small fix-ups on top of the mimalloc 3.5.3 vendor, one for each red leg.
Checked locally on macOS: the test runner builds, and the mem suite passes (72/72). A make dry run shows the new define on both mimalloc compile lines for a MinGW build and absent otherwise. The MSan-only block type-checks against the real sanitizer and mimalloc headers. Windows and x86-64 MSan cannot be reproduced here, so the hosted legs above are the gate. |
The MSan output handler checked msg[0] before marking the text initialized, so MSan reported the handler's own read (tests/test_main.c:2151). Unpoison first, then test for the empty replay. Signed-off-by: Martin Vogel <[email protected]>
|
Pushed ebdf9ba on top of the two fix-ups: the MSan-only output handler now marks mimalloc's text initialized before reading even its first byte (it checked msg[0] one line too early, which the test-msan leg reported at tests/test_main.c:2151). Windows is already confirmed fixed on 0f8b465 (smoke-win, test-windows, windows-guards green); test-msan on this head is the remaining gate. |
Supersedes #2562 by @futile, which found and fixed this; it lands as a maintainer change because we vendor third-party code ourselves. Credit and co-authorship below.
The bug
With mimalloc 3.3.2's release build,
free(NULL)before the allocator's constructor dereferenced a page map that does not exist yet. glibc 2.44'snewlocale()does exactly that during libstdc++ start-up, so every Linux binary built against it died with SIGSEGV beforemain(#2562 has the full analysis). Upstream fixed it in microsoft/mimalloc@b2226c1 (mimalloc #1341), first released in v3.4.4.The change
include/,src/), byte for byte from the tag, then re-apply only the local patches main still needs:src/prim/unix/prim.c: macOS always recommits on decommit (the phys_footprint fix from 63fb7b3).src/prim/osx/alloc-override-zone.c: one comment reworded, becausescripts/security-vendored.shrejects the upstreamfork()spelling even in a comment (wording from fix(memory): update mimalloc to fix glibc 2.44 startup crash #2562). No code change.__DATE__/__TIME__in v3.4.5 (mimalloc Unresolvable calls are bound to any same-named symbol (Jestdescribe, RxJStake) and trace_path exposes no confidence/strategy #1355). Two Makefile comments that pointed at it are updated.-DMI_PROFILE=0for the production allocator (MIMALLOC_CFLAGSin Makefile.cbm). mimalloc 3.5.2+ compiles its allocation-sampling profiler in by default, which puts a sample countdown on the allocation path; the product never uses it. The test build keeps upstream's default.vendored/mimalloc/PATCHES.md: the pin, a re-verification recipe, which check pins each patch and the build flag, and the refresh procedure. THIRD_PARTY.md points at it, the SBOM says 3.5.3, andscripts/vendored-checksums.txtis regenerated (mimalloc lines only).tests/test_prod_allocator_contract.sh, wired intoscripts/test.shright after Step 5 builds the production binary. The unit suites link mimalloc's debug build without the global override, so they cannot see this class of bug. The contract links three small fixtures againstprod_mimalloc.oitself:free(NULL)from an ELF.preinit_arrayhook, before every constructor (Linux), plus a check thatmallocreally is mimalloc;vX.Y.Zwith no build clock, the object is the release build, and it has no profiling line (pinsMI_PROFILE=0);_mi_prim_decommitsetsneeds_recommiton macOS and keeps upstream's value elsewhere.Provenance
v3.5.3(annotated tag objectdeac3dcf5f0614884013d411a5c07e16bec96c9f), commitd4881d338125e1cb7c47ba4cfb398d6f7c0c8d45, treefbc4e8f72287d127c079ada5ede870b7d63e55f5; published GitHub release of 2026-09-17 onmain3. Verified in a fresh bare clone and against the GitHub API.LICENSEblob and theinclude/andsrc/trees had exactly the tag's object ids (50 files). Afterwards the only differences are the two hunks above;vendored/mimalloc/PATCHES.mdhas the one-loop recipe to re-check this from a fresh clone.Verification
needs_recommit=0on macOS; without the comment rewordingscripts/security-vendored.shblocks the upstream line; an object built withMI_PROFILE=1, withMI_DEBUG=2, or with a suffix after the version fails the banner fixture.scripts/security-audit.sh,scripts/security-vendored.shandscripts/security-strings.sh(on the production binary) pass; every--contracts-onlystep passes except Step 0w (language-count), which fails identically on main since dd357a7 and is untouched here.Measurements
Apple M5 Pro, macOS 26.6, full index, 2 interleaved reps (main fceb25d with mimalloc 3.3.2 vs this branch):
Without the macOS recommit patch, the same v3.5.3 tree peaks at 2.09 / 2.04 GB phys_footprint on TypeScript, so that patch is still needed.
Graph quality: identical on both corpora (full node and edge hashes, node census per label, edge census per type, no dangling edges, no duplicate label/qualified-name groups; django 68,022 nodes / 322,464 edges, TypeScript 306,239 / 835,984).
Linux arm64 (Colima, glibc 2.39, gcc 13.3), django, 2 interleaved reps: peak RSS 0.67 / 0.68 GB on main vs 0.69 / 0.68 GB on this branch.
Notes for review
Makefile.cbm(theMI_PROFILE=0flag plus two comments),scripts/test.sh(one new step),scripts/vendored-checksums.txt(mimalloc lines only) andscripts/ci/generate-sbom.py(version string).MI_MINGW_UCRT64is defined (upstream's CMake sets it; our Makefile does not), so our Windows build now also initializes through the constructor-attribute path. The Windows legs and the smoke allocator-ownership check cover this.Credit
The diagnosis, the upstream pointers, the zone-comment wording and the preinit reproduction are @futile's work from #2562; Felix Rath is Co-authored-by on both commits. #2562 will be closed with thanks once this merges.