Merge pull request #22539 from hrydgard/validate-ge-dump-commands

GE dump playback: Validate commands, clamp writes, bound allocations
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-13 10:37:55 +02:00
commit d726e736d5
1 file changed
+130 -24
+130 -24
View File
@@ -476,6 +476,15 @@ void DumpExecute::SyncStall() {
}
void DumpExecute::Registers(u32 ptr, u32 sz) {
// Whole commands only, and a block too big for the list buffer goes in pieces.
sz &= ~3;
const u32 maxChunk = LIST_BUF_SIZE / 2;
while (sz > maxChunk) {
Registers(ptr, maxChunk);
ptr += maxChunk;
sz -= maxChunk;
}
initialClut_ = false;
if (execListBuf == 0) {
u32 allocSize = LIST_BUF_SIZE;
@@ -498,6 +507,11 @@ void DumpExecute::Registers(u32 ptr, u32 sz) {
gpu->EnableInterrupts(true);
}
// Only a crafted dump queues this much between two register blocks.
if (execListQueue.size() * sizeof(u32) + sz + 12 > (size_t)LIST_BUF_SIZE) {
ERROR_LOG(Log::GeDebugger, "Too many queued commands (%d), dropping them", (int)execListQueue.size());
execListQueue.clear();
}
u32 pendingSize = (u32)execListQueue.size() * sizeof(u32);
// Validate space for jump.
u32 allocSize = pendingSize + sz + 8;
@@ -874,14 +888,15 @@ void DumpExecute::Memset(u32 ptr, u32 sz) {
const MemsetCommand *data = (const MemsetCommand *)(pushbuf_.data() + ptr);
if (Memory::IsVRAMAddress(data->dest)) {
const u32 size = Memory::ClampValidSizeAt(data->dest, data->sz);
if (Memory::IsVRAMAddress(data->dest) && size != 0) {
SyncStall();
// Draws the software renderer still has queued come before the write.
gpu->Flush();
// TODO: should probably do this as an operation.
// The software renderer leaves the memset to the caller, like sceKernelMemset does.
if (!gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz))
SetMemory(data->dest, (u8)data->value, data->sz);
if (!gpu->PerformMemorySet(data->dest, (u8)data->value, size))
SetMemory(data->dest, (u8)data->value, size);
}
}
@@ -891,7 +906,8 @@ void DumpExecute::MemcpyDest(u32 ptr, u32 sz) {
void DumpExecute::Memcpy(u32 ptr, u32 sz) {
PROFILE_THIS_SCOPE("ReplayMemcpy");
if (Memory::IsVRAMAddress(execMemcpyDest)) {
sz = Memory::ClampValidSizeAt(execMemcpyDest, sz);
if (Memory::IsVRAMAddress(execMemcpyDest) && sz != 0) {
SyncStall();
gpu->Flush();
CopyToMemory(execMemcpyDest, pushbuf_.data() + ptr, sz);
@@ -978,7 +994,7 @@ void DumpExecute::MarkNeededTargets() {
const bool isTarget = (header[2] & 1) != 0;
const bool unchangedVRAM = (header[2] & 2) != 0;
const u32 start = header[0] & (VRAM_SIZE - 1);
const u32 end = std::min(start + (cmd.sz - 16), VRAM_SIZE);
const u32 end = start + std::min(cmd.sz - 16, VRAM_SIZE - start);
for (u32 b = start >> BLOCK_SHIFT; b < (end + (1 << BLOCK_SHIFT) - 1) >> BLOCK_SHIFT; b++) {
if (unchangedVRAM) {
if (lastSource[b] >= 0 && lastIsTarget[b]) {
@@ -1200,28 +1216,116 @@ ReplayResult DumpExecute::Run() {
return ReplayResult::Done;
}
static bool ReadCompressed(u32 fp, void *dest, size_t sz, uint32_t version) {
// Reads count items, checking the sizes the file declares against the file and each other before
// allocating for them.
template <typename T>
static bool ReadCompressed(u32 fp, u64 fileSize, std::vector<T> &dest, u32 count, uint32_t version) {
u32 compressed_size = 0;
if (pspFileSystem.ReadFile(fp, (u8 *)&compressed_size, sizeof(compressed_size)) != sizeof(compressed_size)) {
return false;
}
u8 *compressed = new u8[compressed_size];
if (pspFileSystem.ReadFile(fp, compressed, compressed_size) != compressed_size) {
delete[] compressed;
const u64 pos = pspFileSystem.GetSeekPos(fp);
if (pos > fileSize || compressed_size > fileSize - pos) {
return false;
}
size_t real_size = sz;
if (version < 5)
snappy_uncompress((const char *)compressed, compressed_size, (char *)dest, &real_size);
else
real_size = ZSTD_decompress(dest, real_size, compressed, compressed_size);
delete[] compressed;
std::vector<u8> compressed(compressed_size);
if (pspFileSystem.ReadFile(fp, compressed.data(), compressed_size) != compressed_size) {
return false;
}
// Far beyond any real dump. A zstd frame doesn't always say how big it is, and a small one can
// expand to gigabytes, so this is what keeps a crafted one from exhausting memory.
const u64 MAX_SIZE = 1024 * 1024 * 1024;
const u64 sz = (u64)count * sizeof(T);
if (sz > MAX_SIZE) {
ERROR_LOG(Log::GeDebugger, "GE dump section too large: %llu bytes", (unsigned long long)sz);
return false;
} else if (version < 5) {
size_t declared = 0;
if (snappy_uncompressed_length((const char *)compressed.data(), compressed_size, &declared) != SNAPPY_OK || declared != sz) {
return false;
}
} else {
// Some older recorders left the size out of the frame.
const unsigned long long declared = ZSTD_getFrameContentSize(compressed.data(), compressed_size);
if (declared != ZSTD_CONTENTSIZE_UNKNOWN && declared != sz) {
return false;
}
}
dest.resize(count);
size_t real_size = (size_t)sz;
if (version < 5) {
if (snappy_uncompress((const char *)compressed.data(), compressed_size, (char *)dest.data(), &real_size) != SNAPPY_OK)
return false;
} else {
real_size = ZSTD_decompress(dest.data(), real_size, compressed.data(), compressed_size);
}
return real_size == sz;
}
static bool ValidateReplayCommands(const std::vector<Command> &commands, const std::vector<u8> &pushbuf) {
for (size_t i = 0; i < commands.size(); ++i) {
const Command &cmd = commands[i];
u32 minSize;
switch (cmd.type) {
case CommandType::INIT:
minSize = 512 * sizeof(u32);
break;
case CommandType::CLUTADDR:
minSize = 2 * sizeof(u32);
break;
case CommandType::MEMSET:
case CommandType::DISPLAY:
minSize = 3 * sizeof(u32);
break;
case CommandType::MEMCPYDEST:
case CommandType::EDRAMTRANS:
minSize = sizeof(u32);
break;
case CommandType::FRAMEBUF0:
case CommandType::FRAMEBUF1:
case CommandType::FRAMEBUF2:
case CommandType::FRAMEBUF3:
case CommandType::FRAMEBUF4:
case CommandType::FRAMEBUF5:
case CommandType::FRAMEBUF6:
case CommandType::FRAMEBUF7:
minSize = 4 * sizeof(u32);
break;
// Any size, including none (version 2 dumps have empty VERTICES and CLUT commands).
case CommandType::REGISTERS:
case CommandType::VERTICES:
case CommandType::INDICES:
case CommandType::CLUT:
case CommandType::TRANSFERSRC:
case CommandType::MEMCPYDATA:
case CommandType::TEXTURE0:
case CommandType::TEXTURE1:
case CommandType::TEXTURE2:
case CommandType::TEXTURE3:
case CommandType::TEXTURE4:
case CommandType::TEXTURE5:
case CommandType::TEXTURE6:
case CommandType::TEXTURE7:
minSize = 0;
break;
default:
ERROR_LOG(Log::GeDebugger, "Unsupported GE dump command: %d", (int)cmd.type);
return false;
}
// Old recorders didn't align anything, so only the range and size are checked.
if (cmd.ptr > pushbuf.size() || cmd.sz > pushbuf.size() - cmd.ptr || cmd.sz < minSize) {
ERROR_LOG(Log::GeDebugger, "Invalid GE dump command %d: type %d, offset %u, size %u",
(int)i, (int)cmd.type, cmd.ptr, cmd.sz);
return false;
}
}
return true;
}
static u32 LoadReplay(const std::string &filename) {
PROFILE_THIS_SCOPE("ReplayLoad");
@@ -1230,11 +1334,12 @@ static u32 LoadReplay(const std::string &filename) {
g_cancelled = false;
u32 fp = pspFileSystem.OpenFile(filename, FILEACCESS_READ);
Header header;
pspFileSystem.ReadFile(fp, (u8 *)&header, sizeof(header));
const u64 fileSize = pspFileSystem.GetFileInfoByHandle(fp).size;
Header header{};
const size_t headerRead = pspFileSystem.ReadFile(fp, (u8 *)&header, sizeof(header));
u32 version = header.version;
if (memcmp(header.magic, HEADER_MAGIC, sizeof(header.magic)) != 0 || header.version > VERSION || header.version < MIN_VERSION) {
if (headerRead != sizeof(header) || memcmp(header.magic, HEADER_MAGIC, sizeof(header.magic)) != 0 || header.version > VERSION || header.version < MIN_VERSION) {
ERROR_LOG(Log::GeDebugger, "Invalid GE dump or unsupported version");
pspFileSystem.CloseFile(fp);
return 0;
@@ -1266,12 +1371,9 @@ static u32 LoadReplay(const std::string &filename) {
u32 bufsz = 0;
pspFileSystem.ReadFile(fp, (u8 *)&bufsz, sizeof(bufsz));
lastExecCommands.resize(sz);
lastExecPushbuf.resize(bufsz);
bool truncated = false;
truncated = truncated || !ReadCompressed(fp, lastExecCommands.data(), sizeof(Command) * sz, header.version);
truncated = truncated || !ReadCompressed(fp, lastExecPushbuf.data(), bufsz, header.version);
truncated = truncated || !ReadCompressed(fp, fileSize, lastExecCommands, sz, header.version);
truncated = truncated || !ReadCompressed(fp, fileSize, lastExecPushbuf, bufsz, header.version);
pspFileSystem.CloseFile(fp);
@@ -1279,6 +1381,10 @@ static u32 LoadReplay(const std::string &filename) {
ERROR_LOG(Log::GeDebugger, "Truncated GE dump detected - can't replay");
return 0;
}
if (!ValidateReplayCommands(lastExecCommands, lastExecPushbuf)) {
ERROR_LOG(Log::GeDebugger, "Invalid GE dump commands detected - can't replay");
return 0;
}
lastExecFilename = filename;
lastExecVersion = version;