mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 23:40:29 +02:00
Merge pull request #22539 from hrydgard/validate-ge-dump-commands
GE dump playback: Validate commands, clamp writes, bound allocations
This commit is contained in:
1 file changed
+130
-24
+130
-24
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user