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.
This commit is contained in:
J1coding
2026-08-01 14:23:46 +02:00
committed by Jeen
parent 5d346a77ee
commit 766dd7c45e
+22 -3
View File
@@ -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<u32>(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);