From 766dd7c45e9a9d8b051dd1a261ca07181fd492b2 Mon Sep 17 00:00:00 2001 From: J1coding Date: Sat, 1 Aug 2026 14:18:10 +0200 Subject: [PATCH] vtlb: bound the page protection table indexes A tester's Namco Museum 50th Anniversary crash on an M2 iPad turned out to be memory corruption rather than anything to do with the software renderer he thought he was hitting. The store that died was a softmem write through a page table entry whose high word had gone from 1 to 2. ProtMode_Manual is 2, it is a u32, and it lives at offset four of an eight byte record, so something had written a protection mode over the top half of a vtlbdata.pmap pointer. It comes from the fastmem branch of HandlePageFault. PSM resolves the whole physical map rather than just main RAM, so a fault on VU memory arrives here with an offset far past the end of m_PageProtectInfo, which is a fixed 8192 entries. Nothing checked that. The read alone is out of bounds, and when the aliased value happens to equal ProtMode_Write the handler carries on into mmap_ClearCpuBlock and writes. The branch twenty lines below has always had the bound check; this one never did, and now does the same thing. Nothing real is suppressed by it. Anything PSM resolves outside main RAM is ROM or VU memory, neither of which is ever under EE write protection, so the right answer for those faults is the else branch that was already there, handing them to the backpatcher. mmap_MarkCountedRamPage had the same unbounded index and a signed int on top of it, which went negative and indexed backwards whenever the pointer landed below Main. Clang has been warning about that conversion the whole time. Bounded the same way as mmap_GetRamPageInfo, and the warning goes with it. No caller reaches either case today since they all come through mmap_GetRamPageInfo first, so that half is closing a trap rather than fixing live breakage. A lot had to line up, which is why it lasted this long. 16K pages mean VU0 memory can never be folded into fastmem so it faults on every touch, the arena base makes the aliased half read as exactly ProtMode_Write so the guard passes because of what it is corrupting, and the game has to touch VU0 memory from the EE and then reprogram the TLB to spread the poisoned entry around. It writes once and stops, so it leaves nothing behind in the log. --- pcsx2/vtlb.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/pcsx2/vtlb.cpp b/pcsx2/vtlb.cpp index a4fac9b5f1..f28794c521 100644 --- a/pcsx2/vtlb.cpp +++ b/pcsx2/vtlb.cpp @@ -1556,8 +1556,19 @@ void mmap_MarkCountedRamPage(u32 paddr) paddr &= ~__pagemask; + // Same story as the fault handler: anything PSM resolves outside main RAM is + // ROM or VU memory, which is never under EE write protection, so there is + // nothing here to mark. Bounded the way mmap_GetRamPageInfo already does it. + // The old int also went negative for a pointer below Main and indexed + // backwards out of the array. No caller reaches either case today, they all + // come through mmap_GetRamPageInfo first, but it is a nasty thing to leave + // lying around for the next one. uptr ptr = (uptr)PSM(paddr); - int rampage = (ptr - (uptr)eeMem->Main) >> __pageshift; + uptr rampage = ptr - (uptr)eeMem->Main; + if (!ptr || rampage >= Ps2MemSize::ExposedRam) + return; + + rampage >>= __pageshift; // Important: Update the ReverseRamMap here because TLB changes could alter the paddr // mapping into eeMem->Main. @@ -1574,7 +1585,8 @@ void mmap_MarkCountedRamPage(u32 paddr) m_PageProtectInfo[rampage].Mode = ProtMode_Write; HostSys::MemProtect(&eeMem->Main[rampage << __pageshift], __pagesize, PageAccess_ReadOnly()); - vtlb_UpdateFastmemProtection(rampage << __pageshift, __pagesize, PageAccess_ReadOnly()); + // Narrowing is safe, the bound above keeps this under ExposedRam. + vtlb_UpdateFastmemProtection(static_cast(rampage << __pageshift), __pagesize, PageAccess_ReadOnly()); } // offset - offset of address relative to psM. @@ -1607,9 +1619,16 @@ PageFaultHandler::HandlerResult PageFaultHandler::HandlePageFault(void* exceptio // this was inside the fastmem area. check if it's a code page // fprintf(stderr, "Fault on fastmem %p vaddr %08X\n", info.addr, vaddr); + // PSM resolves the whole physical map, not just main RAM, so a fault on + // VU memory or ROM arrives here with an offset way past the end of the + // table. Reading it is out of bounds, and if the aliased value happens to + // match ProtMode_Write we walk into mmap_ClearCpuBlock and write a + // ProtMode over whatever follows the array. The branch below has always + // had this check; this one never did. uptr ptr = (uptr)PSM(vaddr); uptr offset = (ptr - (uptr)eeMem->Main); - if (ptr && m_PageProtectInfo[offset >> __pageshift].Mode == ProtMode_Write) + if (ptr && offset < Ps2MemSize::ExposedRam && + m_PageProtectInfo[offset >> __pageshift].Mode == ProtMode_Write) { // fprintf(stderr, "Not backpatching code write at %08X\n", vaddr); mmap_ClearCpuBlock(offset);