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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#!/usr/bin/env python3
"""Crash-oracle fuzz of the TAs the fault-dump oracle has never been pointed at.
`psp_out/FUZZING-RESULTS.md`'s ~2000-case crash-oracle run was entirely HDCP, and
`hw/gcp_faultoracle_fuzz.py` has since covered GCP. Everything else has only ever been fuzzed
with a status/stall/window oracle, and only over command ids 0..7 at offsets 4..0x80
(`ta_fieldfuzz.py`). This covers the rest with the strongest oracle available.
Command lists are taken from the dispatchers in fw/decomp rather than guessed:
opm 0..11 merit 1..6 wspe 4,5 csu 1..3 oc 1,2 dtm 2..4
auc 1..6 fp 1..7 rap 1..7
Commands already recorded as denial-of-service triggers in fw/REPORT-PSP-DOS-TRIGGERS.md are
skipped, since re-triggering them only costs an ATX power cycle: AUC 6, FP 7, FP 2.
Oracle, strongest first:
1. PSP fault dump C2PMSG 82..88 changes -> TA data abort, i.e. memory corruption
2. ring stops responding -> hang (the known class)
3. a write lands past the declared window
4. stall > 5s, or a status differing from that command's own baseline
usage: AM_RESET=1 PYTHONPATH=/root/psp/tinygrad ta_faultoracle_fuzz.py --apply opm,merit
"""
import argparse
import struct
import sys
import time
DECL = 0x4000 # satisfies every module's minimum-size gate (OPM's 0x3D54 is the largest)
TOTAL = 0x8000
PAT = 0xA5A5A5A5
TAS = {
# OPM cmd 11 hangs at offset 0x8 with 0x1000 (found by this harness); skipped so the
# sweep can continue without an ATX power cycle per run.
'opm': ('ta_25-0-0-81_dc596ba6', list(range(0, 11)), set()),
'merit': ('ta_24-0-0-70_5cd4afda', [1, 2, 3, 4, 5, 6], set()),
# WSPE cmd 5 hangs at offset 0x4 with 0xFFFFFFFF (found by this harness).
'wspe': ('ta_30-0-4-27_ba19a7f6', [4], set()),
'csu': ('ta_42-0-0-6_1c874c3c', [1, 2, 3], set()),
'oc': ('ta_50-0-0-1_cbd47c59', [1, 2], set()),
# DTM cmd 2 hangs at offset 0x4 with 0x1000 (found by this harness).
'dtm': ('ta_18-0-4-19_376639d1', [3, 4], set()),
'auc': ('ta_16-0-0-9_9e9044cf', [1, 2, 3, 4, 5], {6}),
'fp': ('ta_19-0-0-8_7e8be282', [1, 3, 4, 5, 6], {2, 7}),
# RAP cmd 3 hangs at offset 0x4 with 0xFFFFFFFF (found by this harness).
'rap': ('ta_7-0-2-19_f368676e', [1, 2, 4, 5, 6, 7], set()),
}
OFFS = [0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C, 0x20, 0x24, 0x28, 0x2C, 0x30,
0x40, 0x80, 0x100, 0x200, 0x400, 0x414,
0x818, 0x81C, 0x820, 0x824, 0x828, 0x82C, 0x830,
0x1000, 0x2000, 0x3C00, 0x3D00, 0x3D40]
VALUES = [0xFFFFFFFF, 0x80000000, 0x1000]
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--apply', action='store_true')
ap.add_argument('tas')
args = ap.parse_args()
if not args.apply:
print('dry run')
return 0
import glob
from tinygrad import Device
import tinygrad.runtime.autogen.am.am as am
adev = Device["AMD"].iface.dev_impl
psp = adev.psp
FAULT = [getattr(adev, 'mmMP0_SMN_C2PMSG_%d' % n) for n in range(82, 89)]
def faults():
try:
return tuple(r.read() for r in FAULT)
except Exception:
return None
def alive():
try:
return bool(psp.is_sos_alive())
except Exception:
return None
def alloc(n):
pa = adev.mm.palloc(n, align=0x1000, zero=True, boot=False)
return pa, adev.paddr2mc(pa)
def load(blob):
ap_, amc = alloc(0x80000)
cp, cmc = alloc(TOTAL)
n = (len(blob) + 3) & ~3
adev.vram.view(ap_, 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 = 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 = DECL
try:
r = psp._ring_submit(c)
return r.resp.status & 0xFFFFFFFF, r.resp.session_id, cp
except Exception as e:
try:
return int(str(e).strip().split()[-1]) & 0xFFFFFFFF, None, cp
except Exception:
return -1, None, cp
def inv(sid, cp, cmd, off=None, val=None):
buf = bytearray(struct.pack('<I', PAT) * (TOTAL // 4))
struct.pack_into('<I', buf, 0, cmd)
for o in OFFS:
struct.pack_into('<I', buf, o, 0)
if off is not None:
struct.pack_into('<I', buf, off, val)
adev.vram.view(cp, TOTAL)[:] = 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, cmd
t0 = time.time()
try:
st = psp._ring_submit(c).resp.status & 0xFFFFFFFF
except Exception:
st = -1
dt = time.time() - t0
adev.gmc.flush_hdp()
out = bytes(adev.vram.view(cp, TOTAL)[:])
bey = sum(1 for o in range(DECL, TOTAL, 4)
if struct.unpack_from('<I', out, o)[0] != PAT)
return st, dt, bey
f0 = faults()
print('baseline fault regs=%s sos_alive=%s' % ([hex(x) for x in f0] if f0 else None, alive()),
flush=True)
grand = 0
for name in args.tas.split(','):
stem, cmds, skip = TAS[name]
cands = glob.glob('/root/psp/contall/' + stem + '*')
if not cands:
print('%-6s binary not found (%s)' % (name, stem), flush=True)
continue
st, sid, cp = load(open(cands[0], 'rb').read())
if st != 0 or sid is None:
print('%-6s LOAD %#x' % (name, st), flush=True)
continue
found = []
n = 0
for cmd in cmds:
if cmd in skip:
continue
base, _, _ = inv(sid, cp, cmd)
stop = False
for off in OFFS:
for v in VALUES:
s2, dt, bey = inv(sid, cp, cmd, off, v)
n += 1
f = faults()
if f is not None and f0 is not None and f != f0:
found.append(('FAULT-DUMP', cmd, hex(off), hex(v),
[hex(x) for x in f])); stop = True
elif s2 == -1:
found.append(('RING-DEAD', cmd, hex(off), hex(v))); stop = True
elif bey:
found.append(('OUT-OF-WINDOW', cmd, hex(off), hex(v), bey)); stop = True
elif dt > 5:
found.append(('STALL', cmd, hex(off), hex(v), round(dt, 1)))
elif s2 != base:
found.append(('status', cmd, hex(off), hex(v), hex(base), hex(s2)))
if stop:
break
if stop:
break
if stop:
break
grand += n
hard = [x for x in found if x[0] in ('FAULT-DUMP', 'RING-DEAD', 'OUT-OF-WINDOW')]
print('%-6s %4d cases %s' % (name, n, hard[:2] if hard else
('%d soft diffs' % len(found) if found else 'clean')), flush=True)
if hard and hard[0][0] in ('FAULT-DUMP', 'RING-DEAD'):
print(' stopping: %s on %s' % (hard[0][0], name), flush=True)
break
print('\ntotal %d cases; sos_alive=%s fault regs=%s'
% (grand, alive(), [hex(x) for x in (faults() or [])]), flush=True)
return 0
if __name__ == '__main__':
raise SystemExit(main())
|