aboutsummaryrefslogtreecommitdiffstats
path: root/f32
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-28 16:19:54 +0200
committeruser <user@clank>2026-09-28 16:19:54 +0200
commit2bac54abd7ea6f9fbaba4cffe54c252cbf61fa97 (patch)
tree6894e29da02ef53c7dc1f2d3bbf74c53e06c3bd7 /f32
parentReach the PSR-SU dirty-rect path by state injection; writes stay bounded (diff)
downloadamd-psp-re-2bac54abd7ea6f9fbaba4cffe54c252cbf61fa97.tar.gz
amd-psp-re-2bac54abd7ea6f9fbaba4cffe54c252cbf61fa97.tar.xz
amd-psp-re-2bac54abd7ea6f9fbaba4cffe54c252cbf61fa97.zip
Synthesise the corpus: test the strongest compound, and reorder the targets
A deliberate pass over all findings asking whether any two combine into something neither is alone. The strongest candidate is tested and falsified. FINDING-10 leaks memory with no memory-safety bug -- it turns an unreadable value into measurable time -- and its only real limitation is that the DMUB inbox is kernel-only. SDMA's POLL_REGMEM has the identical shape on queues that ARE user-submittable, and since a poll writes nothing, the gate covering SRBM_WRITE and POLL_REG_WRITE_MEM need not have applied to it. On silicon the privileged control shows the oracle plainly (match 0.08 ms vs mismatch 3002 ms) and it vanishes at RB_PRIV=0, where both outcomes become an indistinguishable hang. SDMA's register boundary is therefore consistent across write, read-to-memory and read-by-timing -- a stronger claim than any single probe made. Recorded alongside it: an untested cross-engine question (SDMA's security is one bit, DMCUB's types 1/3 are an intentional arbitrary register write -- can one engine set another's privilege bit?); the systemic pattern that memory is confined by hardware while registers are confined by firmware convention, which is exactly what rollback undoes; and that DMUB rollback needs no power cycle, unlike sOS rollback. The useful output is a reordered target list. The only user-submittable surfaces here are SDMA queues (audited, gated) and PM4 -> MEC, which went unaudited only because MEC was unreadable. It is RV64 and readable now, and the SDMA methodology transfers directly, so MEC-PM4-FOOTHOLD.md becomes the highest-expected-value target left. Rejected compounds are kept too, including FINDING-10 against the HDCP canary fence (no PSP memory aperture reachable from DMCUB) and FINDING-1 against the PSR state machine (the host owns CW6 anyway, which lowers FINDING-1's severity).
Diffstat (limited to 'f32')
-rw-r--r--f32/hw/sdma_pollreg_oracle.py122
1 files changed, 122 insertions, 0 deletions
diff --git a/f32/hw/sdma_pollreg_oracle.py b/f32/hw/sdma_pollreg_oracle.py
new file mode 100644
index 0000000..6a76c47
--- /dev/null
+++ b/f32/hw/sdma_pollreg_oracle.py
@@ -0,0 +1,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())