Do not dereference page->heap on the cross-thread free path while mi_heap_delete can free the heap - #24
Closed
robobun wants to merge 2 commits into
Closed
Conversation
…oncurrent mi_heap_delete can free the heap mi_heap_delete claims each page of the heap, re-points page->heap to the main heap, and then frees the heap struct. A mi_free from another thread runs concurrently with that. Two places on its path read page->heap at a point where the deleter does not wait for them: - mi_stat_free (MI_STAT>0) read page->heap->subproc before it owns the page. The heap struct can be freed between the two loads, and the free list link is written over heap->subproc (its first field), so the next load is garbage and subproc->theap_meta faults. This is the SIGSEGV in test-heap-mt (heap-free-during-delete-overlap) under load. The stats now find the subproc through the page's arena, which is constant for the lifetime of the page (and through the current subproc for an OS allocated page), and the meta theap test compares the page's thread id with MI_THREADID_DETACHED directly. - mi_arenas_page_free_prim read page->heap->subproc after it cleared the page's bit in the heap's arena_pages. That bit is what the deleter waits on, so the heap can be gone by then. Read the subproc once, before the bit is cleared. Document the rule on mi_page_heap and on the page->heap field.
…e is freed mi_bitmap_clear updates the chunkmap of the bitmap after it clears the bit. The bit is what a concurrent mi_heap_delete waits on before it frees the heap and its arena_pages (mi_heap_visit_page_at), so that chunkmap update could touch a freed arena_pages. Add mi_bitmap_clear_no_chunkmap, which leaves the chunkmap bit set (allowed: it means the chunk may have bits set, and the pages bitmap is only ever visited, never searched), and use it in mi_arenas_page_free_prim. Narrow the comment on mi_page_heap to what the delete actually waits for.
This was referenced Aug 21, 2026
test-heap-aba: regression test for the theap cache ABA on a reused heap address (stacked on #25)
#26
Closed
Collaborator
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.
Problem
mimalloc-test-heap-mt(heap-free-during-delete-overlap) segfaults under load. Top frame:mi_stat_free,src/free.c:718,subproc->theap_meta, frommi_free_block_mt.mi_stat_freereadspage->heap->subprocbefore it owns the page. A concurrentmi_heap_deletere-pointspage->heap(arena.c:2774) and frees the heap struct (heap.c:225). The free list link lands onheap->subproc, its first field.mi_arenas_page_free_prim(by reading): the deleter stops waiting once the page'sarena_pagesbit is clear (arena.c:1141), but after thatmi_bitmap_clearstill updates the chunkmap insidearena_pages, and line 1166 readspage->heap->subproc. The deleter frees both.Fix
mi_stat_freetakes the subproc from the page's arena (page->memid, constant for the life of the page) and detects a meta page withMI_THREADID_DETACHED, the only thread idtheap_metahas. It no longer readspage->heap.mi_arenas_page_free_primreads the subproc first and clears the bit with the newmi_bitmap_clear_no_chunkmap, so the clear is its last access to the heap. A stale chunkmap bit is allowed by the bitmap design, and thepagesbitmap is only visited, never searched.mi_page_heap. Stats attribution is unchanged for arena pages.ctestin Debug and Release (notes).Background
mi_heap_deletekeeps the live blocks: it moves the pages to the main heap and frees the heap struct. The blocks are freed later withmi_free, so the two run concurrently on one heap.page->xthread_free. A cross-thread free takes it to collect an abandoned page. The deleter spins while a freer has it (mi_heap_visit_page_at), until the freer gives it up or clears the page'sarena_pagesbit.mi_stat_freeexists only withMI_STAT > 0(debug builds). Thefree_primpart is compiled in release builds.Notes
Repro (Debug,
MI_DEBUG_FULL=ON,MI_BUILD_TESTS=ON,MI_OVERRIDE=OFF): run 6 to 8 copies oftaskset -c <two cpus> ./mimalloc-test-heap-mt 1000(or1500) at once. Unfixed: 7 crashes in 32 runs, every backtracemi_stat_free free.c:718 <- mi_free_block_mt free.c:69 <- mi_free <- t1_freer test-heap-mt.c:67. The fault address is 0: a#GPon the0xDF..poison or on the free list link. Fixed: 0 crashes in 60 runs (36 with the first commit, 24 with both). A single unloaded run passes either way.TSAN (
CC=clang -DMI_DEBUG_TSAN=ON,./mimalloc-test-heap-mt 50), unfixed. Both reports havemi_stat_free free.c:717as the reader:Fixed: exit 0 and no reports, 3 of 3 runs with each commit. The "Debug, TSAN" job in
.github/workflows/test.yamlwould show this, but that workflow triggers ondev*branches only, so it does not run forbun-dev3-v2.How much the existing test exercises the second site: little. When the delete starts, only the pages that were full beyond
page_full_retainare abandoned (one page per iteration in Debug because of the padding, none in Release). The other pages are still held by the theap and are moved without contention. #25 adds a variant where the allocating thread exits first, so every page is abandoned and claimed by the freers. Thefree_primchange is by reading, and by that variant under TSAN.The OS page case in
mi_stat_free: a page without an arena uses_mi_subproc(). That is the same subproc as before unless a program uses several subprocs, the page is OS allocated, and the free comes from a thread of another subproc. Stats only.Release exposure, the question in the report that led here.
mi_stat_freeis compiled out withMI_STAT=0, which bun's release builds use. Thefree_primpart is compiled in, but it needs a concurrentmi_heap_delete, and bun never callsmi_heap_delete:src/mimalloc_sys/mimalloc.rsbinds onlymi_heap_destroy, and the JSC structure heap is never deleted. In release the stalesubprocvalue was also only used for OS pages (_mi_arenas_freeignores it for arena memory). So this is not a candidate for the bun 1.4 crashes (BUN-40BH family), and bun does not need a pin bump for it: it can ride the next bump. Upstreamdev3has the same code at all three places.Remaining
mi_heap_deletevsmi_freeissues, not changed here:heap.c:236), and_mi_heap_detach_theapsleavestheap->heapset. A freer that used the heap before still finds that theap through its thread local (_mi_page_associated_theap_peek, fromfree.c:369andarena.c:1269) and reads it while or after it is freed. A test variant (pages abandoned by an exited allocator thread, freers that allocated from the heap once) crashes in the first iteration in Debug, inmi_theap_matches_threadthroughprim-tls.h:422. Fixed in mi_heap_delete: detach the theaps before the pages are moved and free them after #25, stacked on this PR._mi_heap_visit_blocksskips an OS allocated page that a concurrent freer owns instead of waiting (arena.c:2653), and an OS page still held by a theap is not visited at all. WithMIMALLOC_DISALLOW_ARENA_ALLOC=1the test tripsarena.c:2800at once, and a single threaded delete followed by frees tripspage-queue.c:200. OS pages andmi_heap_deleteneed a design of their own.Test suite. Debug: 20 of 21 pass.
test-purge-holesfails inunformed-tail(test-purge-holes.c:1643) the same way on the unfixed tree on this machine, 3 of 3 runs. Release: 20 of 20 pass. In a TSAN configuration,ninjafor all targets fails to linktest-theap-sentinelandmi-heapviewon the unfixed tree as well, so the TSAN binaries were built per target.src/static.ccompiles without warnings as C++ with clang 21 in bun's debug, release and ASAN configurations.