Repository navigation
fix(mem): buddy leak, split/coalesce race and fragmentation on MCUs - #103
Merged
Merged
Conversation
ttak_mem_buddy_free() deferred every block through ttak_epoch_retire(), which only frees on ttak_epoch_reclaim(). Nothing calls that on bare metal, so freed blocks never came back, and each retire node took a pool block of its own: a 200-byte alloc/free loop ran the 64 KiB pool dry after ~100 rounds. Free at once, as ttak_dangerous_free() already does on hosted builds. Freeing at once exposed a race the leak had hidden. The free lists were guarded by one lock per order tier, but splitting a block and coalescing its buddy touch two orders. A block being split keeps in_use == 0 until the split ends, so a concurrent free could merge it as a free buddy and hand the same memory out twice (FreeRTOS asserted on a future mutex handle that had been overwritten). Take one lock for alloc, free and defragmentation.
Every thread that entered an epoch allocated a state and a list node that were never freed, since reclaim walks the list without a lock. On a static pool these piled up for each thread ever created and pinned blocks across the pool. Mark a node free when its thread deregisters, or, without native TLS, when the thread exits, and let the next thread to register take it over. Memory now follows the peak number of live threads.
ttak_mem_header_t was 64-byte aligned everywhere, padding it to 128 bytes; with the 64-byte buddy header a 20-byte future took a 256-byte block. Cache-line alignment buys nothing on a Cortex-M, so bare-metal builds now align headers (and payloads) to 8 bytes via TTAK_MEM_HEADER_ALIGN: the header shrinks to 88 bytes and the buddy header to 24. Hosted builds keep 64. The VMA region also placed its 20-byte block header in a slot sized for ttak_mem_header_t, wasting 64-108 bytes per allocation.
LIFO free lists reused whichever block was freed last, scattering small objects across the pool until no large block could coalesce: with 64 futures outstanding, test_sharded_sched failed a 1 KiB allocation while ~40 KiB sat free in 256- and 512-byte pieces. Static pools (anti_fragmentation) now keep each list sorted by address, packing allocations toward the low end. ttak_mem_buddy_get_stats() reports capacity, bytes in use and the largest free block. test_buddy covers the leak, coalescing, payload alignment, concurrent split/coalesce and epoch-state reuse; test_sharded_sched holds 64 futures on bare metal again.
TinyCC 0.9.27 compiles as C99, so <stdalign.h> turns _Alignas into an aligned attribute that it ignores on struct members and static arrays. The buddy header shrank to 32 bytes and ttak_mem_header_t lost its alignment, so buddy and ttak_mem_alloc() payloads were misaligned (the new test_payload_alignment caught it). Pad the buddy header by hand, align each segment start inside the allocator, and drop the <stdalign.h> macro under TinyCC so its built-in _Alignas applies.
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.
Summary
#102 had to shrink
test_sharded_schedto 16 outstanding futures on MCUs, because the 64 KiB buddy pool ran out at 64. I wrote that off as fragmentation, but it turned out to be mostly a leak plus a race, with per-object overhead and LIFO reuse on top. All four are fixed here, and the test holds 64 futures on hardware again.fix(buddy)ttak_mem_buddy_free()deferred throughttak_epoch_retire(), which frees only onttak_epoch_reclaim(), and nothing calls that on bare metal. Freed buddy blocks never came back. Each retire node also took a 128 B pool block. A 200 B alloc/free loop ran out after 102 rounds.ttak_dangerous_free()already does on hosted builds (free()).fix(buddy)in_use == 0until the split ends, so a concurrent free could merge it and hand the same memory out twice. Under FreeRTOS this showed up as aconfigASSERTon a future's mutex handle that had been overwritten.fix(epoch)feat(mem)ttak_mem_header_twas 64 B aligned, so 128 B. Add the buddy header and a 20 B future took a 256 B block. The VMA block header (20 B) also sat in a slot sized for the 128 B mem header.TTAK_MEM_HEADER_ALIGN: the mem header shrinks to 88 B and the buddy header to 24 B. Hosted builds keep 64. VMA uses its real block-header size.fix(mem)(TinyCC)<stdalign.h>turns_Alignasinto an attribute, which TinyCC ignores on struct members. This was already broken: the buddy header was 32 B andttak_mem_header_tlost its alignment, so payloads were misaligned. The new alignment test caught it._Alignaskeyword.feat(buddy)anti_fragmentation) keep free lists sorted by address. Addttak_mem_buddy_get_stats()(capacity, bytes in use, largest free block).Behaviour changes
ttak_mem_alloc()now returns pointers aligned to 8 B, not 64 B. For DMA buffers on Cortex-M7 cache lines, build with-DTTAK_MEM_HEADER_ALIGN=32(documented in the README; tested with 32/32).EMBEDDED=1. Their header size, alignment and allocation paths are unchanged.Measurements (QEMU FreeRTOS,
test_sharded_schedwith 64 futures, 20 runs each)configASSERTfailures and timeouts (the race)Testing
test_sharded_schedandtest_buddymps2-an500FreeRTOS / bare metaltest_sharded_sched(64 futures) andtest_buddy, all passingTTAK_MEM_HEADER_ALIGN=32test_buddy,test_future_release,test_sharded_sched,test_thread,test_epoch_grace,test_pool(TSan:test_buddy,test_epoch_grace)test_buddyis new. It covers the leak, coalescing, payload alignment, a concurrent split/coalesce churn, and epoch-state reuse. Run against the old allocator, it fails at allocation 128 with the old free, and segfaults 5 times out of 5 with per-tier locks.