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
|
#!/usr/bin/env python3
"""Compound test: is SDMA's POLL_REGMEM a register read oracle for an *unprivileged* queue?
This comes from putting two findings together.
* DMCUB FINDING-10 (`../../dmcub/AUDIT-DMCUB-05-REGSEQ.md`): `REG_REG_WAIT` leaks
memory because it converts a value the host cannot read into something the host
*can* measure -- the time the command takes to retire. No memory-safety bug is
involved; the channel is the wait itself.
* SDMA (`../AUDIT-SDMA-01.md`): register *access* is gated on `RB_CNTL.RB_PRIV`
[HW] -- `SRBM_WRITE` (write) and `POLL_REG_WRITE_MEM` (read-into-memory) both
refuse on an unprivileged queue.
`POLL_REGMEM` (op 8, sub_op 0) is the same shape as REG_REG_WAIT: it polls a register
until `(value & mask) FUNC reference`. Critically it **writes nothing**, so the gating
argument that covers the other two ("don't let an unprivileged queue read registers
into its memory") may simply not have been applied to it. If it is ungated, then the
FINDING-10 technique transplants onto an engine whose queues *are* user-submittable,
and an unprivileged process gets an arbitrary register read oracle at one bit per
packet -- which none of the DMCUB findings can offer, because that inbox is kernel-only.
Packet (navi10_sdma_pkt_open.h):
dw0 header: op=8, sub_op=0, func<<28 (3 = equal), mem_poll<<31 (0 = register)
dw1 addr_lo = register index << 2 (amdgpu's emit_reg_wait convention)
dw2 addr_hi = 0
dw3 value = reference
dw4 mask
dw5 interval/retry count
Oracle: set a scratch register to a known value, then submit a matching poll and a
non-matching poll and compare retire times. A non-match must spin (long); a match must
retire immediately. Run with RB_PRIV=1 (control) and RB_PRIV=0 (the question), using the
live-flip method -- a queue configured unprivileged from scratch does not fetch at all.
usage: PYTHONPATH=/root/psp/tinygrad sdma_pollreg_oracle.py --apply
"""
import argparse
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sdma_srbm_probe import Probe, SDMA_OP_NOP # noqa: E402
SDMA_OP_POLL_REGMEM = 8
FUNC_EQUAL = 3
# DW5 = interval (bits 0..15) | retry_count (bits 16..27). A retry_count of 0 means
# poll forever, which wedges the queue on a mismatch and cost the first run of this
# test its RB_PRIV=0 case. Bound it so a mismatch retires by itself while still taking
# far longer than a match.
INTERVAL = 0x40
RETRY_COUNT = 0x80
DW5 = (INTERVAL & 0xffff) | ((RETRY_COUNT & 0xfff) << 16)
def poll_pkt(reg_index, reference, mask, func=FUNC_EQUAL, mem_poll=0):
hdr = (SDMA_OP_POLL_REGMEM | (0 << 8) | ((func & 7) << 28)
| ((mem_poll & 1) << 31))
return [hdr, (reg_index << 2) & 0xffffffff, 0,
reference & 0xffffffff, mask & 0xffffffff, DW5]
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')
return 0
from tinygrad import Device
adev = Device["AMD"].iface.dev_impl
treg = adev.reg(args.target)
tidx = treg.addr[0]
p = Probe(adev)
MAGIC = 0x5AA50FF0
print('oracle target %s (host idx 0x%x -> SRC 0x%x), magic 0x%08x' % (
args.target, tidx, tidx << 2, MAGIC))
out = {}
for priv in (1, 0):
# re-establish a working privileged queue each round, then flip the single
# bit under test, so a wedge in one round cannot silently void the next
p.setup(rb_priv=1)
if priv == 0:
p.reg('_RB_CNTL').update(rb_priv=0)
treg.write(MAGIC)
ok, _ = p.submit([SDMA_OP_NOP])
if not ok:
print(' RB_PRIV=%d: queue not running -- inconclusive' % priv)
out[priv] = None
continue
# match: (reg & 0xffffffff) == MAGIC -> should retire at once
okm, tm = p.submit(poll_pkt(tidx, MAGIC, 0xffffffff), timeout_s=3.0)
# mismatch: same register, wrong reference -> must spin
okx, tx = p.submit(poll_pkt(tidx, MAGIC ^ 0xffffffff, 0xffffffff), timeout_s=3.0)
sep = (tx > tm * 3) and (tx - tm) > 0.001
print(' RB_PRIV=%d match %.2f ms (consumed=%s) | mismatch %.2f ms '
'(consumed=%s) | separable=%s' % (
priv, 1000 * tm, okm, 1000 * tx, okx, sep))
out[priv] = (okm, tm, okx, tx, sep)
print('\n=== result')
if not out.get(1) or not out.get(0):
print(' inconclusive: a control failed')
elif out[1][4] and out[0][4]:
print(' ORACLE ON AN UNPRIVILEGED QUEUE: POLL_REGMEM timing reveals register')
print(' content with RB_PRIV=0, on the same queue where SRBM_WRITE and')
print(' POLL_REG_WRITE_MEM are both refused. FINDING-10 transplanted to a')
print(' user-submittable engine.')
elif out[1][4] and not out[0][4]:
print(' gated: the timing difference exists only with RB_PRIV=1, so the poll')
print(' is covered by the same gate as the other register accesses.')
else:
print(' no timing separation even with RB_PRIV=1 -- the packet did not poll a')
print(' register as intended; no conclusion.')
print('\nqueue alive: %s' % p.submit([SDMA_OP_NOP])[0])
return 0
if __name__ == '__main__':
raise SystemExit(main())
|