Files
ppsspp/Core/Util/BlockAllocator.cpp
Henrik RydgårdandClaude Opus 5 879baa6412 Add unit tests for BlockAllocator
BlockAllocator backs sceKernelAllocPartitionMemory and the various partition
allocators, and had no coverage at all. It's pure address bookkeeping - no real
memory - so it can be checked hard: after any sequence of operations the blocks
must still tile the range exactly, with no gaps, overlaps or strays, and
GetTotalFreeBytes/GetLargestFreeBlockSize must agree with what's really in the
list. ValidateAllocator() rebuilds the list through the public accessors and
asserts all of that, and it runs after every step of the randomised sections.

Covers bottom-up and top-down allocation, grain rounding, rejection of zero and
oversized requests, splitting and re-merging on free, double free and free of an
address that was never allocated, AllocAt including unaligned positions and
collisions, AllocAligned with alignments coarser than the allocator's grain,
filling the range completely and draining it, a range whose size isn't a
multiple of the grain, and two randomised churn loops - one plain alloc/free,
one mixing in aligned allocations and AllocAt to reach block layouts the simple
loop never produces. Fixed seeds, so a failure reproduces.

No bugs found - the allocator holds up. Verified the tests aren't vacuous by
injecting two plausible bugs: dropping the forward merge in MergeFreeBlocks, and
an off-by-one in the bottom-up fit check. Both are caught, the second pinpointed
to a specific churn iteration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 14:50:51 +02:00

547 lines
14 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <cstring>
#include "Common/Log.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/StringUtils.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/Util/BlockAllocator.h"
#include "Core/Reporting.h"
// Slow freaking thing but works (eventually) :)
BlockAllocator::~BlockAllocator()
{
Shutdown();
}
void BlockAllocator::Init(u32 rangeStart, u32 rangeSize, bool suballoc) {
Shutdown();
rangeStart_ = rangeStart;
rangeSize_ = rangeSize;
//Initial block, covering everything
top_ = new Block(rangeStart_, rangeSize_, false, NULL, NULL);
bottom_ = top_;
suballoc_ = suballoc;
}
void BlockAllocator::Shutdown()
{
while (bottom_ != NULL)
{
Block *next = bottom_->next;
delete bottom_;
bottom_ = next;
}
top_ = NULL;
}
u32 BlockAllocator::AllocAligned(u32 &size, u32 sizeGrain, u32 grain, bool fromTop, const char *tag)
{
// Sanity check
if (size == 0 || size > rangeSize_) {
ERROR_LOG(Log::sceKernel, "Clearly bogus size: %08x - failing allocation", size);
return -1;
}
// It could be off step, but the grain should generally be a power of 2.
if (grain < grain_)
grain = grain_;
if (sizeGrain < grain_)
sizeGrain = grain_;
// upalign size to grain
size = (size + sizeGrain - 1) & ~(sizeGrain - 1);
if (!fromTop)
{
//Allocate from bottom of mem
for (Block *bp = bottom_; bp != NULL; bp = bp->next)
{
Block &b = *bp;
u32 offset = b.start % grain;
if (offset != 0)
offset = grain - offset;
u32 needed = offset + size;
if (b.taken == false && b.size >= needed)
{
if (b.size == needed)
{
if (offset >= grain_)
InsertFreeBefore(&b, offset);
b.taken = true;
b.SetAllocated(tag, suballoc_);
return b.start;
}
else
{
InsertFreeAfter(&b, b.size - needed);
if (offset >= grain_)
InsertFreeBefore(&b, offset);
b.taken = true;
b.SetAllocated(tag, suballoc_);
return b.start;
}
}
}
}
else
{
// Allocate from top of mem.
for (Block *bp = top_; bp != NULL; bp = bp->prev)
{
Block &b = *bp;
u32 offset = (b.start + b.size - size) % grain;
u32 needed = offset + size;
if (b.taken == false && b.size >= needed)
{
if (b.size == needed)
{
if (offset >= grain_)
InsertFreeAfter(&b, offset);
b.taken = true;
b.SetAllocated(tag, suballoc_);
return b.start;
}
else
{
InsertFreeBefore(&b, b.size - needed);
if (offset >= grain_)
InsertFreeAfter(&b, offset);
b.taken = true;
b.SetAllocated(tag, suballoc_);
return b.start;
}
}
}
}
//Out of memory :(
ListBlocks(LogLevel::LINFO);
ERROR_LOG(Log::sceKernel, "Block Allocator (%08x-%08x) failed to allocate %i (%08x) bytes of contiguous memory", rangeStart_, rangeStart_ + rangeSize_, size, size);
return -1;
}
u32 BlockAllocator::Alloc(u32 &size, bool fromTop, const char *tag)
{
// We want to make sure it's aligned in case AllocAt() was used.
return AllocAligned(size, grain_, grain_, fromTop, tag);
}
u32 BlockAllocator::AllocAt(u32 position, u32 size, const char *tag)
{
CheckBlocks();
if (size > rangeSize_) {
ERROR_LOG(Log::sceKernel, "Clearly bogus size: %08x - failing allocation", size);
return -1;
}
// Downalign the position so we're allocating full blocks.
u32 alignedPosition = position;
u32 alignedSize = size;
if (position & (grain_ - 1)) {
DEBUG_LOG(Log::sceKernel, "Position %08x does not align to grain.", position);
alignedPosition &= ~(grain_ - 1);
// Since the position was decreased, size must increase.
alignedSize += position - alignedPosition;
}
// Upalign size to grain.
alignedSize = (alignedSize + grain_ - 1) & ~(grain_ - 1);
// Tell the caller the allocated size from their requested starting position.
size = alignedSize - (position - alignedPosition);
Block *bp = GetBlockFromAddress(alignedPosition);
if (bp != NULL)
{
Block &b = *bp;
if (b.taken)
{
ERROR_LOG(Log::sceKernel, "Block allocator AllocAt failed, block taken! %08x, %i", position, size);
return -1;
}
else
{
// Make sure the block is big enough to split.
if (b.start + b.size < alignedPosition + alignedSize)
{
ERROR_LOG(Log::sceKernel, "Block allocator AllocAt failed, not enough contiguous space %08x, %i", position, size);
return -1;
}
//good to go
else if (b.start == alignedPosition)
{
if (b.size != alignedSize)
InsertFreeAfter(&b, b.size - alignedSize);
b.taken = true;
b.SetAllocated(tag, suballoc_);
CheckBlocks();
return position;
}
else
{
InsertFreeBefore(&b, alignedPosition - b.start);
if (b.size > alignedSize)
InsertFreeAfter(&b, b.size - alignedSize);
b.taken = true;
b.SetAllocated(tag, suballoc_);
return position;
}
}
}
else
{
ERROR_LOG(Log::sceKernel, "Block allocator AllocAt failed :( %08x, %i", position, size);
}
//Out of memory :(
ListBlocks(LogLevel::LINFO);
ERROR_LOG(Log::sceKernel, "Block Allocator (%08x-%08x) failed to allocate %i (%08x) bytes of contiguous memory", rangeStart_, rangeStart_ + rangeSize_, alignedSize, alignedSize);
return -1;
}
void BlockAllocator::MergeFreeBlocks(Block *fromBlock)
{
VERBOSE_LOG(Log::sceKernel, "Merging Blocks");
Block *prev = fromBlock->prev;
while (prev != NULL && prev->taken == false)
{
VERBOSE_LOG(Log::sceKernel, "Block Alloc found adjacent free blocks - merging");
prev->size += fromBlock->size;
if (fromBlock->next == NULL)
top_ = prev;
else
fromBlock->next->prev = prev;
prev->next = fromBlock->next;
delete fromBlock;
fromBlock = prev;
prev = fromBlock->prev;
}
if (prev == NULL)
bottom_ = fromBlock;
else
prev->next = fromBlock;
Block *next = fromBlock->next;
while (next != NULL && next->taken == false)
{
VERBOSE_LOG(Log::sceKernel, "Block Alloc found adjacent free blocks - merging");
fromBlock->size += next->size;
fromBlock->next = next->next;
delete next;
next = fromBlock->next;
}
if (next == NULL)
top_ = fromBlock;
else
next->prev = fromBlock;
}
bool BlockAllocator::Free(u32 position)
{
Block *b = GetBlockFromAddress(position);
if (b && b->taken)
{
NotifyMemInfo(suballoc_ ? MemBlockFlags::SUB_FREE : MemBlockFlags::FREE, b->start, b->size, "");
b->taken = false;
MergeFreeBlocks(b);
return true;
}
else
{
ERROR_LOG(Log::sceKernel, "BlockAllocator : invalid free %08x", position);
return false;
}
}
bool BlockAllocator::FreeExact(u32 position)
{
Block *b = GetBlockFromAddress(position);
if (b && b->taken && b->start == position)
{
NotifyMemInfo(suballoc_ ? MemBlockFlags::SUB_FREE : MemBlockFlags::FREE, b->start, b->size, "");
b->taken = false;
MergeFreeBlocks(b);
return true;
}
else
{
ERROR_LOG(Log::sceKernel, "BlockAllocator : invalid free %08x", position);
return false;
}
}
BlockAllocator::Block *BlockAllocator::InsertFreeBefore(Block *b, u32 size)
{
Block *inserted = new Block(b->start, size, false, b->prev, b);
b->prev = inserted;
if (inserted->prev == NULL)
bottom_ = inserted;
else
inserted->prev->next = inserted;
b->start += size;
b->size -= size;
return inserted;
}
BlockAllocator::Block *BlockAllocator::InsertFreeAfter(Block *b, u32 size)
{
Block *inserted = new Block(b->start + b->size - size, size, false, b, b->next);
b->next = inserted;
if (inserted->next == NULL)
top_ = inserted;
else
inserted->next->prev = inserted;
b->size -= size;
return inserted;
}
void BlockAllocator::CheckBlocks() const
{
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
{
const Block &b = *bp;
if (b.start > 0xc0000000) { // probably free'd debug values
ERROR_LOG_REPORT(Log::HLE, "Bogus block in allocator");
}
// Outside the valid range, probably logic bug in allocation.
if (b.start + b.size > rangeStart_ + rangeSize_ || b.start < rangeStart_) {
ERROR_LOG_REPORT(Log::HLE, "Bogus block in allocator");
}
}
}
const char *BlockAllocator::GetBlockTag(u32 addr) const {
const Block *b = GetBlockFromAddress(addr);
// Unlike the other accessors here, this used to dereference a possibly-null
// block unconditionally. Callers (e.g. NetAdhocCommon.cpp) pass the result
// straight into strcmp() against an expected tag as part of recovering from a
// stale address left over from an old/corrupt savestate, so return "" rather
// than nullptr - it simply won't match, correctly triggering their recovery
// path instead of crashing in strcmp().
return b ? b->tag : "";
}
// Not inline: IsBlockFree() in the header calls this, so it has to have external linkage.
// (An inline definition in a .cpp is only usable within that .cpp - MSVC happens to emit a
// linkable COMDAT anyway, but clang follows the standard and the Android unittest link fails.)
BlockAllocator::Block *BlockAllocator::GetBlockFromAddress(u32 addr)
{
for (Block *bp = bottom_; bp != NULL; bp = bp->next)
{
Block &b = *bp;
if (b.start <= addr && b.start + b.size > addr)
{
// Got one!
return bp;
}
}
return NULL;
}
const BlockAllocator::Block *BlockAllocator::GetBlockFromAddress(u32 addr) const
{
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
{
const Block &b = *bp;
if (b.start <= addr && b.start + b.size > addr)
{
// Got one!
return bp;
}
}
return NULL;
}
u32 BlockAllocator::GetBlockStartFromAddress(u32 addr) const
{
const Block *b = GetBlockFromAddress(addr);
if (b)
return b->start;
else
return -1;
}
u32 BlockAllocator::GetBlockSizeFromAddress(u32 addr) const
{
const Block *b = GetBlockFromAddress(addr);
if (b)
return b->size;
else
return -1;
}
void BlockAllocator::ListBlocks(LogLevel level) const {
GENERIC_LOG(Log::sceKernel, level, "-----------");
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
{
const Block &b = *bp;
GENERIC_LOG(Log::sceKernel, level, "Block: %08x - %08x size %08x taken=%i tag=%s", b.start, b.start+b.size, b.size, b.taken ? 1:0, b.tag);
}
GENERIC_LOG(Log::sceKernel, level, "-----------");
}
u32 BlockAllocator::GetLargestFreeBlockSize() const
{
u32 maxFreeBlock = 0;
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
{
const Block &b = *bp;
if (!b.taken)
{
if (b.size > maxFreeBlock)
maxFreeBlock = b.size;
}
}
if (maxFreeBlock & (grain_ - 1))
WARN_LOG_REPORT(Log::HLE, "GetLargestFreeBlockSize: free size %08x does not align to grain %08x.", maxFreeBlock, grain_);
return maxFreeBlock;
}
u32 BlockAllocator::GetTotalFreeBytes() const
{
u32 sum = 0;
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
{
const Block &b = *bp;
if (!b.taken)
{
sum += b.size;
}
}
if (sum & (grain_ - 1))
WARN_LOG_REPORT(Log::HLE, "GetTotalFreeBytes: free size %08x does not align to grain %08x.", sum, grain_);
return sum;
}
void BlockAllocator::DoState(PointerWrap &p)
{
// v2: compact per-block form — no per-block Section and no per-save tag
// re-zeroing (tags are zero-padded at write time now). v1 states still
// load through the old form.
auto s = p.Section("BlockAllocator", 1, 2);
if (!s)
return;
const bool compact = s >= 2;
int count = 0;
if (p.mode == p.MODE_READ)
{
Shutdown();
Do(p, count);
bottom_ = new Block(0, 0, false, NULL, NULL);
bottom_->DoState(p, compact);
--count;
// A corrupt/malicious savestate could claim an enormous block count. Each
// block needs at least sizeof(start)+sizeof(size)+sizeof(taken)+sizeof(tag)
// bytes in the stream. Reject totally outlandish values, and also if count is now sub-zero.
size_t maxRemainingBlocks = p.Remaining() / (sizeof(u32) + sizeof(u32) + 1 + 32);
if (count < 0) {
count = 0;
p.SetError(PointerWrap::ERROR_FAILURE);
} else if ((size_t)count > maxRemainingBlocks) {
count = (int)maxRemainingBlocks;
p.SetError(PointerWrap::ERROR_FAILURE);
}
top_ = bottom_;
for (int i = 0; i < count; ++i)
{
top_->next = new Block(0, 0, false, top_, NULL);
top_->next->DoState(p, compact);
top_ = top_->next;
}
}
else
{
_assert_(bottom_ != nullptr);
for (const Block *bp = bottom_; bp != NULL; bp = bp->next)
++count;
Do(p, count);
bottom_->DoState(p, compact);
--count;
Block *last = bottom_;
for (int i = 0; i < count; ++i)
{
last->next->DoState(p, compact);
last = last->next;
}
}
Do(p, rangeStart_);
Do(p, rangeSize_);
Do(p, grain_);
}
BlockAllocator::Block::Block(u32 _start, u32 _size, bool _taken, Block *_prev, Block *_next)
: start(_start), size(_size), taken(_taken), prev(_prev), next(_next)
{
// Zero the whole tag up front so serialization can store it raw without
// a per-save strlen+memset (see the compact DoState form).
memset(tag, 0, sizeof(tag));
truncate_cpy(tag, "(untitled)");
}
void BlockAllocator::Block::SetAllocated(const char *_tag, bool suballoc) {
NotifyMemInfo(suballoc ? MemBlockFlags::SUB_ALLOC : MemBlockFlags::ALLOC, start, size, _tag ? _tag : "");
memset(tag, 0, sizeof(tag));
if (_tag)
truncate_cpy(tag, _tag);
else
truncate_cpy(tag, "---");
}
void BlockAllocator::Block::DoState(PointerWrap &p, bool compact)
{
if (compact) {
Do(p, start);
Do(p, size);
Do(p, taken);
DoArray(p, tag, sizeof(tag));
return;
}
auto s = p.Section("Block", 1);
if (!s)
return;
Do(p, start);
Do(p, size);
Do(p, taken);
// Since we use truncate_cpy, the empty space is not zeroed. Zero it now.
// This avoids saving uninitialized memory.
size_t tagLen = strlen(tag);
memset(tag + tagLen, 0, sizeof(tag) - tagLen);
DoArray(p, tag, sizeof(tag));
}