aboutsummaryrefslogtreecommitdiffstats
path: root/mes/hw/mec_pm4_priv_probe.py
blob: 74b71ba6abcf9e4be37c67124e9187e7e1531273 (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
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
"""Can an *unprivileged* compute queue write a register via PM4 WRITE_DATA?

This is the decisive question for the PM4 surface, and it is the direct analogue of
the SDMA result. On SDMA, register access turned out to be gated on one bit
(`RB_CNTL.RB_PRIV`), proven on silicon three ways (`../../f32/AUDIT-SDMA-01.md`).
PM4 has the same shape and much higher stakes, because PM4 packets are submitted by
**unprivileged user queues** — so if the equivalent gate is missing or ineffective,
that is a genuine user->kernel escalation rather than the driver-only findings the
DMCUB work produced (`../../dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md`).

`PACKET3_WRITE_DATA` with `dst_sel = 0` writes a **memory-mapped register** named by
the packet. The MQD field `cp_hqd_pq_control.priv_state` (and `kmd_queue`) marks a
queue privileged; tinygrad's AM driver sets both **only for KIQ**, so an ordinary
compute ring created through `AM_GFX.setup_ring(..., kiq_xcc=-1)` is exactly the
unprivileged case a user process would get.

Experiment, mirroring the SDMA probes:
  1. bring up an ordinary (unprivileged) compute queue;
  2. host writes a magic into a scratch register, then clears it;
  3. submit WRITE_DATA(dst_sel=register, addr=scratch, data=MAGIC);
  4. read the register back.

A control matters as much as the result: a NOP packet must retire (so we know the
queue runs at all), otherwise "nothing was written" proves nothing — that is the trap
that made the first SDMA RB_PRIV=0 run inconclusive.

usage: PYTHONPATH=/root/psp/tinygrad mec_pm4_priv_probe.py --apply
"""
import argparse
import ctypes
import struct
import time

PACKET3_NOP = 0x10
PACKET3_WRITE_DATA = 0x37
MAGIC = 0x7E5701CE


def pkt3(op, count):
    return (3 << 30) | ((count & 0x3FFF) << 16) | ((op & 0xFF) << 8)


def write_data_reg(reg_index, value):
    """WRITE_DATA, dst_sel=0 (mem-mapped register), wr_confirm=1."""
    ctrl = (0 << 8) | (1 << 20)          # dst_sel bits 11:8, wr_confirm bit 20
    return [pkt3(PACKET3_WRITE_DATA, 3), ctrl, reg_index & 0xffffffff, 0,
            value & 0xffffffff]


def write_data_mem(addr_mc, value):
    """Same packet, dst_sel=5 (memory). This is the encoding control: if the memory
    form lands while the register form does not, then the packet, the queue and the
    submission path are all correct and the difference is the privilege of the
    *destination* -- not a malformed packet, which would produce an identical
    'nothing was written'."""
    ctrl = (5 << 8) | (1 << 20)
    return [pkt3(PACKET3_WRITE_DATA, 3), ctrl,
            addr_mc & 0xffffffff, (addr_mc >> 32) & 0xffffffff, value & 0xffffffff]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--apply', action='store_true')
    ap.add_argument('--target', default='regGRBM_SCRATCH_REG0')
    ap.add_argument('--kiq', action='store_true',
                    help='run on a PRIVILEGED (KIQ) queue instead. This is the other '
                         'half of the control: if the register write lands here and '
                         'not on an ordinary queue, the refusal is about privilege. '
                         'If it is refused here too, then WRITE_DATA->register is '
                         'simply unsupported on MEC and no privilege claim holds.')
    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]

    ring_size = 0x1000
    ring = adev.mm.palloc(ring_size, zero=True, boot=False)
    rptr = adev.mm.palloc(0x1000, zero=True, boot=False)
    wptr = adev.mm.palloc(0x1000, zero=True, boot=False)
    eop = adev.mm.palloc(0x1000, zero=True, boot=False)
    mc = adev.paddr2mc
    print('target %s idx 0x%x; ring mc 0x%x' % (args.target, tidx, mc(ring)))

    # ordinary compute queue: kiq_xcc=-1 => priv_state/kmd_queue NOT set
    doorbell = adev.gfx.setup_ring(ring_addr=mc(ring), ring_size=ring_size,
                                   rptr_addr=mc(rptr), wptr_addr=mc(wptr),
                                   eop_addr=mc(eop), eop_size=0x1000,
                                   idx=0, aql=False)
    if args.kiq:
        # Live-flip the queue's own privilege bit rather than standing up a KIQ (whose
        # MQD slots tinygrad does not allocate for us). Same technique as the SDMA
        # RB_PRIV test: change one bit on a queue that is already running, so the only
        # difference between the two runs is the privilege state.
        adev.gfx._grbm_select(me=1, pipe=0, queue=0)
        ctl = adev.reg('regCP_HQD_PQ_CONTROL')
        before_ctl = ctl.read()
        ctl.write(before_ctl | adev.reg('regCP_HQD_PQ_CONTROL').encode(priv_state=1,
                                                                      kmd_queue=1))
        print('  CP_HQD_PQ_CONTROL 0x%08x -> 0x%08x (priv_state/kmd_queue set)' % (
            before_ctl, ctl.read()))
        adev.gfx._grbm_select()
    print('%s queue up, doorbell 0x%x' % (
        'PRIVILEGED (priv_state=1)' if args.kiq else 'unprivileged compute', doorbell))

    pos = [0]

    def submit(dwords, timeout_s=1.0):
        off = pos[0] * 4
        blob = struct.pack('<%dI' % len(dwords), *dwords)
        adev.vram.view(ring + off, len(blob))[:] = blob
        adev.gmc.flush_hdp()
        pos[0] += len(dwords)
        adev.vram.view(wptr, 8)[:] = struct.pack('<Q', pos[0])
        adev.gmc.flush_hdp()
        adev.doorbell64[doorbell] = pos[0]
        t0 = time.time()
        while time.time() - t0 < timeout_s:
            r = struct.unpack('<Q', bytes(adev.vram.view(rptr, 8)[:]))[0]
            if r >= pos[0]:
                return True, time.time() - t0
            time.sleep(0.002)
        return False, time.time() - t0

    ok, dt = submit([pkt3(PACKET3_NOP, 0), 0])
    print('control: NOP retired=%s (%.1f ms)' % (ok, 1000 * dt))
    if not ok:
        print('\n  queue is not running -- INCONCLUSIVE. A "no write" result here would')
        print('  prove nothing, so no claim is made.')
        return 0

    # encoding control first: the register attempt may fault the queue
    dst = adev.mm.palloc(0x1000, zero=True, boot=False)
    adev.vram.view(dst, 4)[:] = struct.pack('<I', 0)
    adev.gmc.flush_hdp()
    okm, dtm = submit(write_data_mem(mc(dst), MAGIC))
    got = struct.unpack('<I', bytes(adev.vram.view(dst, 4)[:]))[0]
    print('control: WRITE_DATA(dst_sel=mem) retired=%s (%.1f ms) -> memory 0x%08x %s' % (
        okm, 1000 * dtm, got, 'WROTE' if got == MAGIC else 'not written'))
    if got != MAGIC:
        print('\n  the packet encoding itself does not work, so the register result')
        print('  below would be meaningless. No claim made.')
        return 0

    treg.write(0)
    before = treg.read()
    ok2, dt2 = submit(write_data_reg(tidx, MAGIC))
    after = treg.read()
    print('WRITE_DATA(dst_sel=reg) retired=%s (%.1f ms): %s 0x%08x -> 0x%08x' % (
        ok2, 1000 * dt2, args.target, before, after))

    print('\n=== result  (queue privilege: %s)' % (
        'priv_state=1' if args.kiq else 'priv_state=0 (unprivileged)'))
    if after == MAGIC and not args.kiq:
        print('  UNPRIVILEGED PM4 REGISTER WRITE: a compute queue with priv_state=0')
        print('  wrote a host-chosen register. On a system with user-submitted queues')
        print('  that is a user->kernel primitive.')
    elif after == MAGIC and args.kiq:
        print('  expected: a privileged queue may write registers. This is the other')
        print('  half of the control -- paired with the unprivileged run refusing the')
        print('  same packet, it shows the refusal is about PRIVILEGE, not about')
        print('  WRITE_DATA->register being unsupported on MEC.')
    elif after == before:
        print('  refused: the register is unchanged while the queue keeps running, so')
        print('  PM4 register writes are gated for unprivileged queues -- the same')
        print('  shape as SDMA RB_PRIV.')
    else:
        print('  unexpected value 0x%08x; not interpretable' % after)
    ok3, _ = submit([pkt3(PACKET3_NOP, 0), 0])
    print('  queue alive after: %s' % ok3)
    return 0


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