Problem
PoolAllocator::Free asserts that the pointer is in-range and chunk-aligned, but does NOT detect double-free. A second call to Free with the same pointer passes both assertions (pointer is still valid by both checks) and corrupts the free list by inserting a cycle.
Consequence
The next Allocate() call after a double-free returns a pointer that is simultaneously live in two places. Writes through either alias corrupt the other. In Release builds this is a silent memory corruption with no crash until much later.
Fix
Add a per-chunk IsAllocated bitset or, simpler, validate that the pointer's PoolFreeNode::Next field looks like a valid free-list node before inserting (a heuristic, not a guarantee). For Debug builds only, a per-chunk uint8_t Allocated flag is acceptable overhead.
Reproducer
PoolAllocator pool;
pool.Initialize(&arena, 1024, 128);
void* p = pool.Allocate();
pool.Free(p);
pool.Free(p); // silent corruption — no assert fires
void* q = pool.Allocate();
void* r = pool.Allocate(); // q == r — both aliases of the same chunk
References
memory-architecture.md — PoolAllocator Safety Invariants section (double-free note)
Problem
PoolAllocator::Freeasserts that the pointer is in-range and chunk-aligned, but does NOT detect double-free. A second call toFreewith the same pointer passes both assertions (pointer is still valid by both checks) and corrupts the free list by inserting a cycle.Consequence
The next
Allocate()call after a double-free returns a pointer that is simultaneously live in two places. Writes through either alias corrupt the other. In Release builds this is a silent memory corruption with no crash until much later.Fix
Add a per-chunk
IsAllocatedbitset or, simpler, validate that the pointer'sPoolFreeNode::Nextfield looks like a valid free-list node before inserting (a heuristic, not a guarantee). For Debug builds only, a per-chunkuint8_t Allocatedflag is acceptable overhead.Reproducer
References
memory-architecture.md — PoolAllocator Safety Invariants section (double-free note)