mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-30 09:25:13 +02:00
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
547 lines
14 KiB
C++
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));
|
|
}
|