#!/usr/bin/env python3 """Sweep the 64-byte PSP ring frame at dword resolution. The fence finding showed the ring frame is an unexamined host-controlled structure. Only `fence_addr` was tested. This sweeps all 16 dwords -- including the reserved area beyond the named fields -- asking of each: * does the command still complete? * does the PSP stay healthy (heartbeat advancing)? * does any write appear OUTSIDE the intended fence target? The last question is the interesting one: a reserved dword that is secretly a second address would show up as a write landing in a canary region. Canary = a 64 KB page filled with a pattern, never named in any frame field; anything written into it came from the PSP acting on a field we did not intend as a destination. Control: the unmodified frame must complete and leave the canary clean. """ import ctypes, glob, struct, sys, time sys.path.insert(0,'/root/psp/hw') from tinygrad import Device import tinygrad.runtime.autogen.am.am as am adev=Device["AMD"].iface.dev_impl; psp=adev.psp RESP=864; FRAME=ctypes.sizeof(am.struct_psp_gfx_rb_frame); step=FRAME//4 def alloc(n): pa=adev.mm.palloc(n,align=0x1000,zero=True,boot=False); return pa,adev.paddr2mc(pa) PAT=0xCCCCCCCC canary,canmc=alloc(0x10000) blob=open(glob.glob('/root/psp/contall/ta_42-0-0-6_1c874c3c*')[0],'rb').read() ap_,amc=alloc(0x80000); cp,cmc=alloc(0x8000); scratch,smc=alloc(0x1000) n=(len(blob)+3)&~3 adev.vram.view(ap_,n)[:]=blob+b'\x00'*(n-len(blob)); adev.gmc.flush_hdp() def hb(): return adev.reg(f"{psp.reg_pref}_81").read() def run(mutate, tag): adev.vram.view(canary,0x10000)[:]=bytes(struct.pack('>32, fence_addr_lo=smc&0xFFFFFFFF, fence_addr_hi=smc>>32) raw=bytearray(memoryview(f).cast('B')) mutate(raw) wmax=psp.ring_size//4 prev=adev.reg(f"{psp.reg_pref}_67").read()%wmax adev.vram.view(psp.ring_paddr+prev*4, FRAME)[:]=bytes(raw) adev.reg(f"{psp.reg_pref}_67").write((prev+step)%wmax) h0=hb(); t0=time.time(); st=None while time.time()-t0<0.6: adev.gmc.flush_hdp() v=struct.unpack_from('>32; t.app_len=len(blob) t.cmd_buf_phy_addr_lo,t.cmd_buf_phy_addr_hi=cmc&0xFFFFFFFF,cmc>>32; t.cmd_buf_len=0x4000 adev.vram.view(psp.cmd_paddr, ctypes.sizeof(c))[:]=memoryview(c).cast('B') adev.vram.view(psp.cmd_paddr+RESP,4,'I')[0]=0xA5A5A5A5; adev.gmc.flush_hdp() wmax=psp.ring_size//4; prev=adev.reg(f"{psp.reg_pref}_67").read()%wmax f=am.struct_psp_gfx_rb_frame(fence_value=1, cmd_buf_addr_lo=adev.paddr2mc(psp.cmd_paddr)&0xFFFFFFFF, cmd_buf_addr_hi=adev.paddr2mc(psp.cmd_paddr)>>32, fence_addr_lo=smc&0xFFFFFFFF, fence_addr_hi=smc>>32) adev.vram.view(psp.ring_paddr+prev*4, FRAME)[:]=bytes(memoryview(f).cast('B')) adev.reg(f"{psp.reg_pref}_67").write((prev+step)%wmax) time.sleep(1.0); adev.gmc.flush_hdp() sid=struct.unpack_from(' sweep void'); sys.exit(2) NAMED={0:'cmd_buf_lo',4:'cmd_buf_hi',12:'fence_lo',16:'fence_hi',20:'fence_value'} # After every mutation re-run the UNMUTATED control. A timeout means nothing unless the PSP # was demonstrably alive before it and the control still passes after it; otherwise every # later row is inherited from one dead PSP, which is how the LOAD_TA sweep produced 160 # phantom hits. only=int(sys.argv[1],0) if len(sys.argv)>1 else None bad=[]; dead_at=None offs=[only] if only is not None else list(range(0,FRAME,4)) for off in offs: for val in (0xFFFFFFFF, canmc&0xFFFFFFFF, 1): def m(r,o=off,v=val): struct.pack_into(' %s'%(off,NAMED.get(off,'reserved'),val,verdict), flush=True) if dead_at: break if dead_at: print(' stopping: PSP dead after +%d=%#x; later rows would be inherited, not measured' %dead_at, flush=True) break print('\nframe sweep: %d real anomalies -> %s'%(len(bad), bad[:6] if bad else 'NONE'), flush=True)