aboutsummaryrefslogtreecommitdiffstats
path: root/hw/frame_sweep.py
blob: 02dd98e7d76b40d25437661daf27bf211f11b301 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/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('<I',PAT)*(0x10000//4))
    c=am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_INVOKE_CMD)
    iv=c.cmd.cmd_invoke_cmd; iv.session_id,iv.ta_cmd_id=sid,0
    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()
    f=am.struct_psp_gfx_rb_frame(fence_value=0x1234,
        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)
    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('<I',bytes(adev.vram.view(psp.cmd_paddr+RESP,4)[:]),0)[0]
        if v!=0xA5A5A5A5: st=v; break
    adev.gmc.flush_hdp()
    can=bytes(adev.vram.view(canary,0x10000)[:])
    dirty=[o for o in range(0,0x10000,4) if struct.unpack_from('<I',can,o)[0]!=PAT]
    alive=hb()!=h0
    return st, dirty, alive
c=am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_TA); t=c.cmd.cmd_load_ta
t.app_phy_addr_lo,t.app_phy_addr_hi=amc&0xFFFFFFFF,amc>>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('<I',bytes(adev.vram.view(psp.cmd_paddr+RESP+4,4)[:]),0)[0]
print('frame=%d bytes, load sid=%s, canary at %#x (never named in a frame)'%(FRAME,sid,canmc), flush=True)
st,d,a=run(lambda r: None, 'control')
print('control: resp=%s canary_dirty=%d alive=%s'%(hex(st) if st is not None else 'TIMEOUT',len(d),a), flush=True)
if st is None: print('control failed -> 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('<I',r,o,v)
        st,d,a=run(m,'off%d'%off)
        cst,cd,ca=run(lambda r: None,'recheck')
        verdict=None
        if d: verdict='CANARY WRITE @%s'%[hex(x) for x in d[:3]]
        elif st is None and cst is None:
            verdict='KILLED THE PSP (control also fails afterwards)'; dead_at=(off,val)
        elif st is None and cst==0:
            verdict='rejected/no-response but PSP survived'
        if verdict and 'rejected' not in verdict:
            bad.append((off,hex(val),verdict))
            print('  +%-3d %-12s val=%#-11x -> %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)