Let GeneralPurposeAllocator retain metadata to report more double frees #8756
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.
Currently
GeneralPurposeAllocator
can only properly report double frees for small allocations from active buckets (buckets that weren't emptied at any point). Double frees for other small allocations and for all large allocations fall through to the "Invalid free" panic inresizeLarge
(assumingconfig.safety
is true). This will show a stack trace for the current free, but it can't show anything for the allocation or the first free. It also can't be recovered from, unlike properly reported double frees, and this may make debugging more onerous.I've added a new flag,
config.retain_metadata
, that makesGeneralPurposeAllocator
retain all empty buckets in a separate list and never remove entries from thelarge_allocations
hash map (only mark them as freed). This way any double free of memory actually allocated from this allocator will always be able to find a set of stack traces to report. The extra storage for the empty bucket list inGeneralPurposeAllocator
and the second stack trace and other bookkeeping inLargeAlloc
is spent only whenretain_metadata
is set. Further, the performance of small allocations is not affected at all. However, because entries are never removed fromlarge_allocations
, lookup is likely to be slower as the map gets bigger. Leaving entries in the hash map was the simplest option and requires no extra allocations be done when freeing large objects, but maintaining a second hash map for dead large allocations is another option if the effect on performance is too high.Another benefit of retaining metadata is that double frees can eventually be caught without having to use
config.never_unmap
and leak memory (all retained metadata is easily freed when the allocator is freed). For now, until #4298 is finished,retain_metadata
should be used withnever_unmap
or else memsets in the allocator interface will cause segfaults. But in either case, because we're retaining metadata, the memory leaked bynever_unmap
can actually now be freed when the metadata is freed because we know exactly what memory was left deliberately unfreed. This should allow checking for double frees during only portions of a long running program with ballooning memory use. To that end, I added a public functionflushRetainedMetadata
that will free all retained metadata and, ifnever_unmap
is set, recover the leaked memory as well. It is present only whenretain_metadata
is true.I'd like to add tests for actually detecting and recovering from double frees but because the stack traces are logged immediately to stderr, I don't know if there's any way to keep a test from being marked as failing. Instead, I have a rather invasive test that just checks that metadata is kept and marked correctly after a free, and that all memory leaked by
never_unmap
is ultimately freed. For general stress testing, I also ran all of test-std with thestd.testing.allocator
modified to setretain_metadata
(with and then withoutnever_unmap
set) and test_runner.zig modified to use a secondGeneralPurposeAllocator
as the backing_allocator for the testing allocator in order catch any leaks of metadata.Since this is a large addition to somewhat intricate code that is also a basic element of the standard library, I'm happy to break down my changes in more detail if helpful.