Repository navigation
Conversation
|
Warning Review limit reachedYour included review limit has been reached. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 29 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset (next review available in 37 seconds), then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 11:36 AM PT - Aug 22nd, 2026
✅ @robobun, your commit b6904087e74e837749fa521aeeef48cdfb154471 passed in 🧪 To try this PR locally: bunx bun-pr 40101That installs a local version of the PR into your bun-40101 --bun |
|
Status: draft. The fix lives in the fork: oven-sh/mimalloc#25 (the How it was reproduced: Bun side: |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The patch format matches the repo's other dep patches (plain --- a/ / +++ b/ headers, applied via git apply --no-index from vendor/mimalloc/), and the patches field on the Dependency spec is an established pattern. Because this is a concurrency-sensitive change to the global allocator's heap-teardown path, a human look at the mimalloc reasoning would still be worthwhile.
What was reviewed
- Patch header format vs.
fetch-cli.ts'sgit applyinvocation and the PR's note aboutdiff --githeaders being silently skipped — this patch has nodiff --gitline, matchingpatches/highway/andpatches/lshpack/. Dependency.patcheswiring inscripts/build/source.tsandfetch-cli.ts— patch content is hashed into the.refidentity, so adding it forces a re-fetch and non-zerogit applyexit fails the build loudly.- The added line mirrors the store the PR says
_mi_tld_detach_theapsalready performs, and uses themi_atomic_store_ptr_releasehelper rather than a plain assignment.
Extended reasoning...
Overview
This PR adds a one-line patch to the vendored mimalloc allocator (patches/mimalloc/theap-cache-aba.patch) and registers it in scripts/build/deps/mimalloc.ts. The patch inserts mi_atomic_store_ptr_release(mi_heap_t, &theap->heap, NULL) into _mi_heap_detach_theaps immediately after the theap is unlinked from its tld list and before the tld lock is released. The stated goal is to close an ABA window where a thread's one-entry _mi_theap_cached slot (keyed only on theap->heap == heap) can match a freshly-created heap that reuses a destroyed heap's address, handing the thread a detached theap whose pages have been returned to the arena.
Security risks
None in the PR-mechanics sense — no untrusted input parsing, no auth, no network. The risk profile is entirely memory safety and concurrency correctness inside the process-wide allocator. If the added store were wrong (wrong lock held, racing with the owning thread's cache read, or the wrong ordering), the failure mode would be heap corruption rather than a security boundary bypass.
Level of scrutiny
High. mimalloc is Bun's global malloc replacement on Linux and the JS heap allocator everywhere; _mi_heap_detach_theaps runs during mi_heap_destroy, which MimallocArena calls on every parser/transpiler arena teardown. Even a one-line change here sits on a path exercised by essentially every request. The PR description is unusually thorough (Sentry correlation, upstream commit that dropped the store, a fork-level repro test in oven-sh/mimalloc#26, lock-safety argument, and an honest note that the Bun-level trigger is unproven). That raises confidence considerably, but per the review guidelines allocator/concurrency changes to a vendored dep are exactly the category where a maintainer should confirm the reasoning rather than an automated review approving on the diff alone.
Other factors
- I confirmed
patchesis a supported field onDependency(source.ts:459) and is used identically by ~12 other deps (highway, libuv, tinycc, lsquic, etc.).fetch-cli.tshashes patch content into the.refstamp and pipes the CRLF-normalized body togit apply --ignore-whitespace --no-index -withcwd: vendor/mimalloc; a non-zero exit throws, so a mis-applying patch would fail the build rather than silently no-op. - The patch header style (no
diff --gitline, plain--- a/src/theap.c) matches existing patches likepatches/highway/silence-warnings.patchandpatches/lshpack/bss-huff-tables.patch, and is consistent with the PR's own note thatdiff --githeaders causegit applyfromvendor/<dep>to treat paths as repo-relative and skip them. - I could not verify the context lines against
vendor/mimalloc/src/theap.clocally (the tarball is fetched at build time), but the PR statesbun bdwas run with the patch applied and several test suites passed, and a context mismatch would failgit applyloudly. - No Bun-level test is added; the PR is upfront that no Bun input is known to reach the ABA and the repro lives in the fork (
test-heap-aba.c). REVIEW.md's "every behavioral change ships an automated test" is in tension here, but for a dependency patch whose only known repro is at the C allocator level this seems like a reasonable, explicitly-stated exception for a maintainer to sign off on.
Deferring rather than approving because allocator lock-ordering and atomic-ordering claims ("the owner cannot be collecting this theap while the lock is held") are the kind of thing a human familiar with the mimalloc fork should confirm.
|
On the two points the review could not check locally:
A maintainer look at the mimalloc side is welcome; the fork PR has the reproducer. |
…en-sh/mimalloc#25, CI stand-in) The dev3 sync (#37367) carried upstream 36e8fc33, which dropped the `theap->heap = NULL` store from the heap-delete detach path. A thread's one-entry theap cache is keyed only by that pointer, so a heap created at the address of a destroyed one matched the stale entry and the thread allocated from a detached theap whose pages had gone back to the arena. Carry the store as a patch on the current pin so CI builds and tests it on every platform. oven-sh/mimalloc#25 owns the change in the fork and oven-sh/mimalloc#26 adds the regression test; this becomes a pin bump once they are merged, and the patch is removed then.
068869b to
b690408
Compare
|
Superseded by #40138 (commit 861e9ae), which bumps mimalloc to oven-sh/mimalloc#27. That PR combines oven-sh/mimalloc#22 to #26 into one heap delete/destroy teardown protocol and replaces the per-reader fixes, including the #25 store this patch carried. Verified at the pinned commit a178e44af6dc2845c793a39fd62da583b3e9f0ff:
Nothing left for this PR to carry. Closing. |
Draft until oven-sh/mimalloc#24, #25 and #26 are merged. Then this becomes a two-line pin bump (
MIMALLOC_COMMITinscripts/build/deps/mimalloc.tsand the literal intest/js/node/process/process.test.js) and the patch file goes away. The patch is here so CI builds and tests the store on every platform in the meantime; it must not merge in this form, because oven-sh/mimalloc#25 rewrites the lines it patches and the next pin bump would failgit apply.Problem
theap->heap = NULLstore from the heap-delete detach path (_mi_heap_detach_theaps,src/theap.c:656at the pin). A thread's one-entry theap cache (_mi_theap_cached, checked only bytheap->heap == heapininclude/mimalloc/prim-tls.h:392) then matches a new heap created at the address of a destroyed one, and the thread allocates from a detached theap whose pages went back to the arena. Upstream had that store before, with a comment naming this exact ABA; the thread-exit path still has it.test-heap-aba(test-heap-aba: regression test for the theap cache ABA on a reused heap address (stacked on #25) mimalloc#26) shows it on the pin: a debug build segfaults inmi_heap_malloc, a release build hands out 127 of 1024 blocks thatmi_heap_ofattributes to no heap.MimallocArenaallocates only on the thread that created or last reset the heap (asserted in debug builds), and a debug build instrumented to abort on the stale cache hit ran 1630 tests without firing. This restores an allocator invariant Bun depends on; it is not tied to a Bun-level symptom.Fix
theap->tldvalid until the theap struct is freed, for a cross-thread free that is mid-flight); test-heap-aba: regression test for the theap cache ABA on a reused heap address (stacked on #25) mimalloc#26 adds the regression test. This PR will pin the fork head that contains both.test-heap-delete-race): the owner cannot be collecting this theap while the lock is held, and after the unlink it no longer sees it.bun bun#25 tree with the test (Linux x64, Debug and Release). Bun:bun bdwith the patch applied, thentest/js/bun/transpiler,test/bundler/bundler_edgecase.test.ts,test/js/web/workers/worker.test.ts(the threeterminate() racesfailures there are debug-build timing and identical on the unpatched pin).Background
mi_heap_tinto per-threadmi_theap_ts._mi_heap_theap(heap)finds the calling thread's theap through a versioned thread-local slot (heap->theap), with_mi_theap_cached(the theap of the last heap this thread used) as a front cache keyed only by thetheap->heappointer. The slot is versioned, so only the cache can go stale.mi_heap_destroydetaches the heap's theaps from every thread's tld list and frees them. A detached theap stays alive while a thread's cache references it (the cache holds a refcount).MimallocArena(Rust) ismi_heap_new+mi_heap_malloc+mi_heap_destroy, used for parser and transpiler arenas.mi_heap_newallocates the heap struct from the main heap, which refreshes the creating thread's cache, so the same-thread destroy-then-new pattern Bun uses does not hit the ABA.Notes
This came out of an investigation of Sentry BUN-4CG0 (
SIGSEGVinJSC::MarkedBlock::Handle::sweepfromLocalAllocator::tryAllocateIn,MarkedBlock::Header::m_vmNULL while theHandleis intact: the 16 KiB block reads back as zeros while JSC owns it). The event rate rose after the dev3 sync, but the signature is older than mimalloc as JSC's allocator: #24194 (2025-10, Bun 1.2.23 on macOS arm64, libpas-backed JSC) is the same frame chain. The cause of that crash is not established by this PR.What was ruled out for BUN-4CG0, with evidence:
MADV_FREE_REUSABLE; the sweep runs on the scavenger thread for a thread parked inkevent/epoll), which fork commit 6df95990 in the same sync made run on nearly every park: the sweep arithmetic (16 KiB and 4 KiB OS pages, large pages, the unformed tail), the park/wake handshake, the abandoned-page claim protocol, theusedaccounting, the arena purge and bitmap paths, the Bun side of the park contract (packages/bun-usockets/src/eventing/epoll_kqueue.c, signal handlers, spawn), and the 101 upstream commits after the sync point were each read end to end. No fault found.MI_DEBUG_FULLbuild of the pinned fork (16 KiB aligned blocks, cross-thread frees, 9000 short-lived threads, heap create/destroy/delete, 770k park handoffs, 900k discards, emulated 16 KiB OS pages, variants for purge delay, commit on demand, reclaim on free): 100k iterations per variant, zero corruption.test/js/bun/transpiler,test/transpiler,test/js/web/workers,test/js/node/worker_threads,test/bundler/bun-build-api.test.ts,bundler_edgecase,bundler_splitting,bundler_bun,bundler_npm,test/cli/install/bun-install.test.ts,test/js/bun/globandtest/bakewithout a hit.patches/need plain--- a/headers; with adiff --githeadergit applyskips them silently fromvendor/<dep>(build: stopgit applyfrom silently skipping dep patches with adiff --githeader #35099).no test proof · iteration 0 · no src or test change; test-proof not applicable