Repository navigation
Maintain the invariant that no empty block sits behind the front block - #21
Merged
d-torrance merged 2 commits intoAug 29, 2026
Merged
Conversation
restoreToGuardPoint emptied every block that was allocated after the
guard point was taken, but never deallocated them. guardPoint() and
freeTopFromOldBlock() both require that a block behind the front block
is not empty, so this left the arena in a state that they mishandle.
It takes two blocks inside the guard to show up, as with only one the
sole empty block is the front block, which is allowed:
memt::Arena arena;
void* a = arena.alloc(1);
{
memt::Arena::Guard guard(arena);
arena.alloc(arena.getMemoryUse() + 1);
arena.alloc(arena.getMemoryUse() + 1);
}
arena.freeTop(a); // a is the only allocation that is still live
That aborts in freeTopFromOldBlock when MEMT_DEBUG is defined. Without
it the position of one block is set to an address inside another, and
getAllocatedMemoryUse() then wraps around to a huge value.
Free them as freeAndAllAfterFromOldBlock already does. The front block
is kept, as it is kept there too. Drop the record of the allocations
in the blocks being freed before freeing them, so that no freed address
is used afterwards.
Note that this is not the only way to get an empty block behind the
front block. growCapacity does it as well, by chaining an empty front
block behind a newly allocated one, and that is not fixed here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alloc() grows the capacity whenever a request does not fit in what is left of the front block, including when the front block is empty. The old front block was then chained behind the new one regardless, so an empty block ended up behind the front block. guardPoint() and freeTopFromOldBlock() both require that this does not happen. No guard point is needed to reach it: memt::Arena a; void* p = a.alloc(1); // block A void* q = a.alloc(a.getMemoryUse() + 1); // block B, A behind it a.freeTop(q); // B is empty now void* r = a.alloc(a.getMemoryUse() + 1); // block C, B empty behind it a.freeTop(r); a.freeTop(p); // p is in A, but B is examined Every call there is allowed. It aborts in freeTopFromOldBlock when MEMT_DEBUG is defined, and otherwise sets the position of one block to an address inside another, after which getAllocatedMemoryUse() wraps around to a huge value. Deallocate the old front block when it is empty. This is done in growCapacity rather than in MemoryBlocks, as BufferPool shares the latter and has no such requirement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
guardPoint()documents an invariant:freeTopFromOldBlock()relies on it too. Nothing maintained it. There are twoindependent ways to end up with an empty block behind the front block, and
this fixes both — one commit each, with a regression test each.
1.
restoreToGuardPointemptied blocks without deallocating themIt reset the position of every block newer than the guard point but never
freed them. Two blocks inside a guard are needed to show it, since with one
the only empty block is the front block, which is allowed:
The commit also drops the
_debugAllocsentries for the blocks being freedbefore freeing them, so no freed address is used afterwards.
2.
growCapacitychained an empty front block behind the new onealloc()grows the capacity whenever a request does not fit in what is leftof the front block — including when that block is empty. No guard point is
involved, so this is reachable from plain
alloc/freeTop:Fixed in
growCapacityrather than inMemoryBlocks, asBufferPoolsharesthe latter and has no such requirement.
Symptoms
Both cases behave identically. With
MEMT_DEBUG,Arena.cppaborts onAssertion 'previous->isInBlock(ptr)' failedinfreeTopFromOldBlock.Without it, the position of one block is set to an address inside another and
getAllocatedMemoryUse()wraps around to a huge value. Using aGuardinstead of the final
freeToptrips!block().previousBlock()->empty()inguardPoint()instead.Case 2 is the more serious of the two: it needs no guard points, and it
corrupts the block chain silently in ordinary release builds.
Verification
26/26 under
cmake -DCMAKE_BUILD_TYPE=Debug, cmake default, and autotools.make distcheckpasses with and without--enable-debug. Clean underAddressSanitizer and UndefinedBehaviorSanitizer.
Each regression test fails before its fix and passes after, in both debug
and release builds, so neither depends on
MEMT_DEBUGbeing defined.A fuzzer doing random valid operations (nested guards, released guards, LIFO
freeTop,freeAndAllAfter, block-forcing allocations) runs clean over 10seeds x 400 trials in both builds, and reproduces the bug on 5 of 5 seeds
against
master.The two commits are independent and either can be dropped without affecting
the other.
🤖 Generated with Claude Code