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
|
#!/usr/bin/env python3
"""Test the SDMA SRBM_WRITE privilege gate on silicon, and name it by behaviour.
Static finding (`../AUDIT-SDMA-01.md`): the `SRBM_WRITE` packet -- which writes a
host-named register -- is gated before it acts:
ldw r5, reg[r0, #0x4a14] ; and #0x200 ; lsr #9 ; bit 9
ldw r5, reg[r0, #0x5968] ; lsr #31 ; OR bit 31
ldw r5, reg[r0, #0x5940] ; lsr #23 ; and #1 ; AND bit 23
and r5, r6, r5
cbz r5, _PKT_0xe00_2 ; -> error 0x3c/0x32
Attempts to *name* those registers by matching the F32 index space against
`gc_11_0_0_offset.h` failed: the best-fitting constant base scored 25/54 against a null
95th percentile of 24, i.e. not significant (see AUDIT-SDMA-01). So the gate is
identified by **behaviour** instead, which is stronger anyway.
Bit 23 is the giveaway: `SDMA*_QUEUE*_RB_CNTL__RB_PRIV` is mask 0x00800000 = bit 23.
If F32 index 0x5940 is the current queue's RB_CNTL, then clearing `RB_PRIV` must turn
an accepted SRBM_WRITE into a rejected one. tinygrad's AM driver sets `rb_priv=1` when
it configures the queue, so both states are reachable.
Experiment, per RB_PRIV setting:
1. host zeroes a scratch register and reads it back (control that we can see it)
2. a NOP packet is submitted and the ring pointer must advance (control that the
queue actually runs)
3. an SRBM_WRITE packet targeting that scratch register with a magic value
4. read the scratch register from the host
Expected if the static reading is right: RB_PRIV=1 -> magic lands; RB_PRIV=0 -> it does
not. Any other outcome falsifies the reading and is reported as such.
Safety: the target is a GRBM scratch register (no side effects). A rejected packet may
halt the queue; AM resets SDMA on the next device open.
usage: PYTHONPATH=/root/psp/tinygrad sdma_srbm_probe.py [--apply]
"""
import argparse
import ctypes
import struct
import sys
import time
SDMA_OP_NOP = 0
SDMA_OP_SRBM_WRITE = 14
MAGIC = 0x5100D00D
class Probe:
def __init__(self, adev, queue=0):
self.adev = adev
self.q = 'regSDMA0_QUEUE%d' % queue
self.ring_size = 0x1000
self.ring_pa = adev.mm.palloc(self.ring_size, zero=True, boot=False)
self.rptr_pa = adev.mm.palloc(0x1000, zero=True, boot=False)
self.wptr_pa = adev.mm.palloc(0x1000, zero=True, boot=False)
self.ring_mc = adev.paddr2mc(self.ring_pa)
self.wptr = 0
def reg(self, suffix):
return self.adev.reg(self.q + suffix)
def setup(self, rb_priv):
a = self.adev
self.reg('_MINOR_PTR_UPDATE').write(0x1)
a.wreg_pair(self.q + '_RB_RPTR', '', '_HI', 0)
a.wreg_pair(self.q + '_RB_WPTR', '', '_HI', 0)
a.wreg_pair(self.q + '_RB_BASE', '', '_HI', self.ring_mc >> 8)
a.wreg_pair(self.q + '_RB_RPTR_ADDR', '_LO', '_HI', a.paddr2mc(self.rptr_pa))
a.wreg_pair(self.q + '_RB_WPTR_POLL_ADDR', '_LO', '_HI', a.paddr2mc(self.wptr_pa))
self.reg('_MINOR_PTR_UPDATE').write(0x0)
# wptr poll left OFF so we submit by writing RB_WPTR directly
self.reg('_RB_CNTL').write(rb_vmid=0, rptr_writeback_enable=1,
rptr_writeback_timer=4, rb_enable=1,
rb_priv=rb_priv,
rb_size=(self.ring_size // 4).bit_length() - 1)
self.reg('_IB_CNTL').update(ib_enable=1)
self.wptr = 0
got = self.reg('_RB_CNTL').read_bitfields().get('rb_priv')
return got
def submit(self, dwords, timeout_s=1.0):
"""Append dwords to the ring, kick RB_WPTR, wait for RB_RPTR to catch up."""
off = (self.wptr * 4) % self.ring_size
blob = struct.pack('<%dI' % len(dwords), *dwords)
self.adev.vram.view(self.ring_pa + off, len(blob))[:] = blob
self.adev.gmc.flush_hdp()
self.wptr += len(dwords)
self.adev.wreg_pair(self.q + '_RB_WPTR', '', '_HI', self.wptr * 4)
t0 = time.time()
while time.time() - t0 < timeout_s:
r = self.adev.rreg(self.adev.reg(self.q + '_RB_RPTR').addr[0])
if r == (self.wptr * 4) & 0xffffffff:
return True, time.time() - t0
time.sleep(0.002)
return False, time.time() - t0
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--apply', action='store_true')
ap.add_argument('--target', default='regGRBM_SCRATCH_REG0')
args = ap.parse_args()
if not args.apply:
print('dry run: pass --apply to touch the device')
return 0
from tinygrad import Device
adev = Device["AMD"].iface.dev_impl
try:
treg = adev.reg(args.target)
except KeyError:
raise SystemExit('no such register: %s' % args.target)
tidx = treg.addr[0]
print('target %s -> absolute index 0x%x' % (args.target, tidx))
p = Probe(adev)
print('ring paddr 0x%x mc 0x%x' % (p.ring_pa, p.ring_mc))
# Phase B trick: a queue configured from scratch with RB_PRIV=0 does not even
# fetch a NOP (the ring fetch itself is privileged), so "no write" there proves
# nothing. Instead bring the queue up privileged, then clear ONLY RB_PRIV on the
# live queue and re-submit: the ring stays configured, so if the NOP still retires
# while SRBM_WRITE stops taking effect, the gate is isolated to RB_PRIV.
results = {}
for priv in (1, 0):
print('\n=== RB_PRIV = %d' % priv)
if priv == 1:
got = p.setup(rb_priv=1)
else:
p.reg('_RB_CNTL').update(rb_priv=0) # live flip, queue stays up
got = p.reg('_RB_CNTL').read_bitfields().get('rb_priv')
print(' RB_CNTL.rb_priv reads back as %s' % got)
treg.write(0)
base = treg.read()
print(' control: host zeroed %s -> reads 0x%08x' % (args.target, base))
ok, dt = p.submit([SDMA_OP_NOP])
print(' control: NOP packet consumed=%s (%.1f ms)' % (ok, 1000 * dt))
if not ok:
print(' !! queue is not running; result for this setting is inconclusive')
results[priv] = None
continue
hdr = SDMA_OP_SRBM_WRITE | (0xf << 28)
magic = MAGIC if priv else (MAGIC ^ 0xffff)
ok2, dt2 = p.submit([hdr, tidx, magic])
val = treg.read()
print(' SRBM_WRITE consumed=%s (%.1f ms) -> %s = 0x%08x %s' % (
ok2, 1000 * dt2, args.target, val,
'WROTE' if val == magic else 'not written'))
results[priv] = (ok2, val == magic)
print('\n=== result')
r1, r0 = results.get(1), results.get(0)
if r1 and r0:
if r1[1] and not r0[1]:
print(' CONFIRMED: SRBM_WRITE executes only with RB_PRIV=1.')
print(' => F32 register index 0x5940 is the queue RB_CNTL, and its bit 23')
print(' (RB_PRIV) is the privilege gate read at 0x0b07-0x0b0a.')
elif r1[1] and r0[1]:
print(' FALSIFIED: the write landed with RB_PRIV=0 too -- the gate is not')
print(' RB_PRIV, or it is satisfied by the OR-term (0x4a14 bit 9 / 0x5968')
print(' bit 31) independently. The static reading needs revisiting.')
elif not r1[1]:
print(' INCONCLUSIVE: the write did not land even with RB_PRIV=1, so this')
print(' setup does not exercise the path (wrong register, aperture, or')
print(' queue state). No conclusion about the gate.')
else:
print(' inconclusive: a control failed')
for rn in ('regSDMA0_STATUS_REG', 'regSDMA0_QUEUE0_CONTEXT_STATUS'):
try:
print('%s = 0x%08x' % (rn, adev.reg(rn).read()))
except KeyError:
pass
return 0
if __name__ == '__main__':
raise SystemExit(main())
|