Skip to content

fix(memory): update mimalloc to fix glibc 2.44 startup crash - #2562

Open
futile wants to merge 2 commits into
DeusData:mainfrom
futile:fix/mimalloc-glibc-startup
Open

futile wants to merge 2 commits into
DeusData:mainfrom
futile:fix/mimalloc-glibc-startup

Conversation

@futile

@futile futile commented Oct 7, 2026

Copy link
Copy Markdown

What does this PR do?

Fix a Linux startup segfault exposed by glibc 2.44 by updating vendored mimalloc from 3.3.2 to 3.4.4 (upstream commit 1f06f694972279bbc7ec72902e8570f5784d0fc9).

During libstdc++ initialization, glibc's newlocale() can call the globally interposed free(NULL) before mimalloc's constructor runs. The old release allocator dereferences its uninitialized page map, leading to this SIGSEGV immediately and reproducibly when the cli binary starts:

$ codebase-memory-mcp cli --progress index_status --project home-felix-gits-pale-darks-online
fish: Job 1, 'codebase-memory-mcp cli --progr…' terminated by signal SIGSEGV (Address boundary error)

mimalloc 3.4.4, the version this PR updates to, includes the upstream fix:

Changes

  • Refresh vendored include/, src/, and LICENSE from the upstream release. The existing reproducible-banner patch is preserved; one macOS comment is reworded to avoid a false positive in the vendored security scanner. Both local differences and the immutable source pin are documented in vendored/mimalloc/PATCHES.md.
  • Update the SBOM version, third-party documentation, and vendored integrity manifest.
  • Add a Linux ELF-preinit regression test linked against prod_mimalloc.o with production allocator interposition. It calls free(NULL) before allocator initialization and then checks normal allocation/free in main. Wire it into the production checks in scripts/test.sh.

The normal sanitizer runner does not enable global allocator interposition, so it cannot cover this startup regression. The new fixture fails with SIGSEGV (exit 139) when linked to the original mimalloc 3.3.2 production object, and passes with 3.4.4.

Validation

Tested locally on x86_64 Linux with GCC 16.2.0 and glibc 2.44:

  • PASS: scripts/build.sh; production binary --version.
  • PASS: bash tests/test_mimalloc_startup.sh build/c, with the failing old-allocator comparison described above.
  • FAIL (exit 1): all listed C suites through the supported iteration entry point: scripts/test.sh --suites "$(build/tests/test-runner --list-suites)" BUILD_DIR=build/tests (ASan + UBSan build). The log contains 832 assertion-failure lines, many from probe fixtures reporting no graph DB, recovering UBSan diagnostics, and a LeakSanitizer report of 13,399 bytes in 11 allocations. These additional failures have not been compared against clean upstream main; they are not claimed to be baseline failures or unrelated to this change.
  • PASS: scripts/smoke-local.sh build/c/codebase-memory-mcp, including indexing, MCP queries, install/uninstall, and daemon cleanup. The UI-only leg was skipped because the frontend was not built.
  • PASS: the reported cli --progress index_status --project ... form, against a freshly indexed private fixture rather than an existing user index.
  • PASS: scripts/security-audit.sh and scripts/security-vendored.sh. The broader make -f Makefile.cbm security target also passed source, binary-string, source-only UI, sandbox-install, and strace network checks, but stopped on a 10-second MCP fuzz timeout (shell injection semicolon). That exact input passed three isolated retries; the subsequent full scripts/security-fuzz.sh retry passed 32/32. The combined security target itself is not claimed as passing, and built-frontend integrity was not verified.

Existing baseline failures

The following checks fail on this branch and were reproduced on a clean checkout of upstream main bf93f0b7c3b82683b3ac3b00bb0342465878cdf8, with the same local tool versions:

  • bash tests/test_version_metadata_contract.sh: the Scoop manifest pin is 0.11.0, equal to the latest stable tag; the contract requires changing that surface's classification from pin:0.11.0 to release and dropping its PIN_REASONS entry. This stops the default scripts/test.sh before the C suites, hence the separate all-suite invocation above.
  • Cppcheck 2.22: src/mcp/mcp.c:962 reports a possible null dereference of allowed. Reproduced independently with make -f Makefile.cbm lint-cppcheck LINT_SRCS=src/mcp/mcp.c. The branch's full lint gate also fails; formatting, NOLINT, memory-core checks, and applicable diff-scoped clang-tidy passed.

The full scripts/lint.sh audit additionally encounters GCC-only warning flags passed to Clang and pre-existing findings in unchanged files. This is not a claim that the full unpatched test/lint suite or GitHub CI was run. Baseline failures were not changed or suppressed in this focused allocator fix. The local pre-commit gate was bypassed because these seemed potentially unrelated to me (the human), and I wanted to see what PR CI says/what discussion says.

AI disclosure

This contribution and description were prepared with an OpenAI coding agent in Pi on behalf of Felix Rath (futile). Felix is the accountable human contributor and explicitly authorized the DCO sign-off on every commit. No private agent-session links are included.

Checklist

  • Every commit is signed off (git commit -s); scripts/check-dco.sh upstream/main..HEAD passed for both commits.
  • Full default tests pass locally — blocked by the unchanged baseline version contract; focused regression and smoke checks pass, but the separate all-suite run also fails as detailed above.
  • Lint passes — unchanged baseline failure described above.
  • New behavior is covered by a reproduce-first regression test.

futile added 2 commits October 7, 2026 18:37
Refresh vendored mimalloc from 3.3.2 to 3.4.4, including the upstream fix for free(NULL) before allocator initialization (microsoft/mimalloc#1341). Preserve and document local patches and refresh SBOM and integrity metadata.

Signed-off-by: Felix Rath <felixm.rath@gmail.com>
Link an ELF preinit fixture against the production allocator so global interposition is exercised before constructors. Run the regression with the production checks in scripts/test.sh.

Signed-off-by: Felix Rath <felixm.rath@gmail.com>
@futile
futile requested a review from DeusData as a code owner October 7, 2026 22:47
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added bug Something isn't working stability/performance Server crashes, OOM, hangs, high CPU/memory priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. dependencies Pull requests that update a dependency file labels Oct 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks for the allocator startup reproduction and for separating the pre-init regression from the broader failing checks. I have read the update and its validation limits. The vendored mimalloc update remains pending review; the un-compared full-suite failures are not being treated as baseline-green evidence.

@DeusData

Copy link
Copy Markdown
Owner

Thank you, @futile -- this is an excellent report and an even better fix. Your analysis is spot on: with a recent glibc, newlocale() can call free(NULL) before mimalloc's constructor runs, and our current release allocator then reads a page map that does not exist yet. We reproduced it with our production allocator flags and a preinit free(NULL), exactly as your test does, and confirmed that the upstream fix (microsoft/mimalloc@b2226c1) makes it go away. We also checked your vendored tree against the upstream v3.4.4 tag -- it matches exactly apart from the two documented local edits, which made the review much easier.

One thing shapes how we land it: we vendor third-party code ourselves, and main now carries a second local mimalloc patch (a macOS memory-footprint change in src/prim/unix/prim.c, which is why this branch conflicts). We are deciding between a small backport of the upstream fix and the full 3.4.4 refresh, and will come back here shortly with the route. Either way your diagnosis and your preinit regression test are the basis, with credit to you. Thank you for the careful write-up and the reproduce-first test.

@DeusData

Copy link
Copy Markdown
Owner

Thank you again, @futile. As promised, here is the route we took.

We went with a full refresh rather than a backport, and since we vendor third-party code ourselves, it lands as a maintainer PR: #2731. It vendors mimalloc v3.5.3, upstream's latest published release (newer than the 3.4.4 in this branch, and it contains microsoft/mimalloc@b2226c1, the fix you identified), and keeps the two local changes main still needs: the macOS decommit change in src/prim/unix/prim.c and your comment rewording in the zone file, which our vendored-code gate still requires. The reproducible-banner edit is no longer needed, because upstream dropped the build clock in v3.4.5. Each remaining local change is now documented in vendored/mimalloc/PATCHES.md and pinned by a check that fails without it.

Your preinit test is the heart of the new contract test: tests/test_prod_allocator_contract.sh links a free(NULL)-from-.preinit_array fixture against the production allocator object, exactly as your test does, and runs on every Linux and macOS leg. On Linux it fails on current main (segfault, every run, with gcc and clang) and passes on the new branch. You are credited as co-author on both commits.

We will close this PR once #2731 is merged. Thank you for the diagnosis, the upstream pointers and the reproduce-first test -- they made this a well-understood fix.

@futile

futile commented Oct 20, 2026

Copy link
Copy Markdown
Author

Thank you very much for the detailed updates, the credit on the commits, and for the nice, improved solution! Feel free to close as you like :)

DeusData added a commit that referenced this pull request Oct 20, 2026
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 <felixm.rath@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Oct 20, 2026
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 <felixm.rath@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

This branch has not been deployed

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

Labels

bug Something isn't working dependencies Pull requests that update a dependency file priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. stability/performance Server crashes, OOM, hangs, high CPU/memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants