diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp index fdae560098..71b63a77bd 100644 --- a/GPU/Debugger/Playback.cpp +++ b/GPU/Debugger/Playback.cpp @@ -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 +static bool ReadCompressed(u32 fp, u64 fileSize, std::vector &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 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 &commands, const std::vector &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;