aboutsummaryrefslogtreecommitdiffstats
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
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).
-rw-r--r--f32/hw/sdma_pollreg_oracle.py122
-rw-r--r--psp_out/INDEX.md31
-rw-r--r--psp_out/SYNTHESIS-CROSS-FINDING-COMPOUNDS.md140
3 files changed, 293 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())
diff --git a/psp_out/INDEX.md b/psp_out/INDEX.md
index fa83e98..e701f37 100644
--- a/psp_out/INDEX.md
+++ b/psp_out/INDEX.md
@@ -368,6 +368,37 @@ indices match `gc_11_0_0_offset.h` = coincidence; a ±0x2000 delta search peaks
and the CI-era opcode table leaves 2.6-3% of gfx11 words unknown — the listing is a
strong draft, not ground truth.
+### Cross-finding synthesis — what compounds, and where it says to look next
+
+`SYNTHESIS-CROSS-FINDING-COMPOUNDS.md`. A deliberate pass over the whole corpus asking
+whether any two findings combine into something neither is alone.
+
+* **Tested and falsified:** FINDING-10's timing technique transplanted onto SDMA's
+ *user-submittable* queues via `POLL_REGMEM` — which writes nothing, so the gate
+ covering the other register accesses need not have applied. On silicon the privileged
+ control shows the oracle plainly (match 0.08 ms vs mismatch 3002 ms) and it
+ **vanishes** at `RB_PRIV=0` (both hang, indistinguishable). SDMA's register boundary
+ is therefore consistent across all three access shapes — write, read-to-memory, and
+ read-by-timing — a stronger statement than any single probe made.
+* **Untested, concrete:** SDMA's register security is *one bit* (`RB_PRIV`), while
+ DMCUB types 1/3 are an intentional arbitrary-register write. Can one engine set
+ another's privilege bit? That would make the gate a convention rather than a boundary.
+* **Systemic pattern:** memory is confined by *hardware* (page tables, CW windows);
+ registers are confined by *firmware convention* — and firmware convention is exactly
+ what rollback undoes. DMCUB has no register boundary at all; only reachability saves
+ it.
+* **Amplification:** DMUB rollback (FINDING-8) needs no PCIe power cycle, unlike
+ sOS rollback, so it is the quieter way to reintroduce any DMCUB defect fixed since
+ 2024-02.
+* **Reordered target list:** the only *user-submittable* surfaces on this card are SDMA
+ queues (audited, gated) and **PM4 → MEC**, which is unaudited because MEC was
+ unreadable — and it is RV64 and readable now. `MEC-PM4-FOOTHOLD.md` is the
+ highest-expected-value target left, with the SDMA methodology directly transferable.
+
+Rejected compounds are recorded too, including FINDING-10 × the HDCP canary fence (no
+PSP memory aperture to read from DMCUB) and FINDING-1 × the PSR state machine (the host
+owns CW6 anyway — this *lowers* FINDING-1's severity).
+
### Reachability: none of the DMCUB findings is exploitable by an unprivileged user
`../dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md`. Stated up front so ten findings are not
diff --git a/psp_out/SYNTHESIS-CROSS-FINDING-COMPOUNDS.md b/psp_out/SYNTHESIS-CROSS-FINDING-COMPOUNDS.md
new file mode 100644
index 0000000..b16d44b
--- /dev/null
+++ b/psp_out/SYNTHESIS-CROSS-FINDING-COMPOUNDS.md
@@ -0,0 +1,140 @@
+# Cross-finding synthesis: what compounds, what does not, and what it implies
+
+A deliberate pass over the whole corpus (145 docs in `psp_out/`, plus `dmcub/`,
+`f32/`, `mes/`, `smu/`) asking one question: do any two findings combine into
+something neither is alone? Recorded because most of the answers are *no*, and the
+no's are as useful as the yes.
+
+--------------------------------------------------------------------------------
+## 1. Tested today: FINDING-10's technique × SDMA's user-reachable queues — **FAILS**
+
+The most promising compound, and it does not work.
+
+DMCUB FINDING-10 leaks PSP-protected memory with no memory-safety bug at all: it
+converts a value the host cannot read into *time* the host can measure. Its fatal
+limitation is reachability — the DMUB inbox is kernel-only, so nothing a user can do
+reaches it (`../dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md`).
+
+SDMA has the identical shape in `POLL_REGMEM`, and unlike DMCUB its queues *are*
+user-submittable. We had proved register **writes** (`SRBM_WRITE`) and **reads into
+memory** (`POLL_REG_WRITE_MEM`) are gated on `RB_CNTL.RB_PRIV` — but a plain poll
+writes nothing, so the reasoning behind that gate need not have been applied to it.
+If ungated, an unprivileged process would get an arbitrary register read oracle at one
+bit per packet: the one thing all ten DMCUB findings cannot offer.
+
+`../f32/hw/sdma_pollreg_oracle.py`, on silicon:
+
+```
+RB_PRIV=1 match 0.08 ms | mismatch 3002 ms separable <- the oracle is real here
+RB_PRIV=0 match 3002 ms | mismatch 3000 ms identical <- no channel
+```
+
+The privileged control is what makes the negative meaningful: the timing channel is
+plainly visible when the queue is allowed to read registers, and **vanishes** when it
+is not — both outcomes become an indistinguishable hang. `POLL_REGMEM` is covered by
+the same gate as the other two.
+
+**So SDMA's register boundary is consistent across all three access shapes** (write,
+read-to-memory, read-by-timing). That is a stronger statement than any single probe
+made, and it is exactly the kind of thing only a compound test produces.
+
+--------------------------------------------------------------------------------
+## 2. Untested and concrete: the cross-engine `RB_PRIV` bridge
+
+SDMA's entire register-access security reduces to **one bit** — `RB_CNTL.RB_PRIV`,
+bit 23 — proven three ways now. Meanwhile DMCUB types 1/3 (`REG_SEQ_*`) are an
+*intentional* arbitrary-register write: `addr = 0x5ffc0000 + 4*idx` with a **full
+32-bit host index** (`../dmcub/AUDIT-DMCUB-03-COMMANDS.md`, and the address is not
+confined to registers — that is what FINDING-10 exploits).
+
+The question neither finding asks alone: **can one on-die engine set another's
+privilege bit?** If DMCUB's aperture reaches SDMA's `RB_CNTL`, then DMCUB is a
+privilege-escalation gadget for SDMA queues — you would not need to defeat `RB_PRIV`,
+you would ask a different engine to grant it.
+
+This does not by itself cross a user/kernel boundary (driving DMCUB needs the kernel),
+but it matters for two reasons: it would show the gate is not a boundary but a
+convention, and it is the natural *second stage* for any future DMCUB code-execution
+result. Directly testable with `dmcub_poc_regseq.py`'s type-3 path plus
+`sdma_srbm_probe.py`'s reader. **Not yet run.**
+
+--------------------------------------------------------------------------------
+## 3. Amplification already half-documented: DMUB rollback is cheaper than sOS rollback
+
+`CONFIRMED-FIRMWARE-ROLLBACK-WORKS.md` established sOS/SYS_DRV rollback and correctly
+connected it to the `.54` fixes. FINDING-8 adds a sharper version of the same idea for
+DMUB, with a detail that changes its practicality:
+
+| | sOS / SYS_DRV rollback | DMUB rollback (FINDING-8) |
+| --- | --- | --- |
+| mechanism | bootloader mailbox at init | `LOAD_IP_FW` **at any time** |
+| precondition | PSP not already running → **PCIe power cycle first** | none; the running DMCUB is simply replaced |
+| binding | version not checked | neither version **nor ASIC** checked |
+
+So the DMUB variant needs no power cycle and leaves the rest of the PSP untouched —
+a quieter way to reintroduce any DMCUB defect fixed since 2024-02, including the ones
+in this tree. The generalisable claim across both: **AMD's signature check on this part
+authenticates the blob but binds nothing about which blob belongs here.**
+
+--------------------------------------------------------------------------------
+## 4. The systemic pattern, stated once
+
+Laying the confirmed boundary results side by side:
+
+| engine | what confines memory access | what confines register access |
+| --- | --- | --- |
+| SDMA | queue VMID page tables [HW] | `RB_PRIV` [HW], all three access shapes |
+| DMCUB | CW windows, host-owned [HW] | **nothing** — types 1/3/4 are by design |
+| MES | (packet-driven, dispatch bounded) | not established |
+
+Two observations fall out that no individual audit states:
+
+* **Memory is confined by hardware (page tables, windows); registers are confined by
+ firmware convention.** Every register-access control found in this tree is a
+ firmware-side check, and firmware checks are exactly what rollback (§3) undoes.
+* **DMCUB has no register boundary at all** and is therefore the weakest engine in the
+ set — which is consistent with FINDING-10 falling out of it. Its saving grace is
+ purely reachability: the inbox is kernel-only.
+
+--------------------------------------------------------------------------------
+## 5. Where this says to look next: MEC / PM4
+
+The reachability analysis is the most valuable thing this pass produced, because it
+reorders the whole target list. On this card the surfaces are:
+
+| surface | who can submit | audited? |
+| --- | --- | --- |
+| DMUB inbox | kernel only | yes — 10 findings, none user-reachable |
+| SDMA queues | **user** (KFD/user queues) | yes — gated, no finding |
+| **PM4 → MEC/CP** | **user** | **no** |
+| PSP TA ring | kernel only | yes (prior corpus) |
+
+`MEC-PM4-FOOTHOLD.md` has sat in this tree unexplored because MEC was unreadable. It
+is not unreadable any more: MEC is **RV64** (`../mes/AUDIT-MES-01.md`), and the exact
+methodology that worked on SDMA now exists —
+recover the packet dispatch table, validate it against the published PM4 opcode
+enums, taint from the command-queue source with **call-clobber-aware** dataflow, then
+hunt the privilege gate and test it on hardware with a live-flip control.
+
+PM4 is submitted directly by unprivileged user queues, and the CP enforces a
+privileged/unprivileged packet distinction in firmware — the same structural shape as
+`RB_PRIV`, on the one engine where a failure *is* a user→kernel escalation. That is the
+highest-expected-value target in the tree, and it is now cheap to start.
+
+--------------------------------------------------------------------------------
+## 6. Compounds considered and rejected
+
+* **FINDING-10 × the HDCP subcmd12 canary fence.** `FINDINGS-SUMMARY.md` blocks that
+ exploit partly on "no canary oracle is constructible". FINDING-10 *is* an oracle, but
+ it reads DMCUB's address space; `FINDING-NO-PSP-MEMORY-APERTURE.md` establishes there
+ is no PSP memory aperture to reach from there. The TA canary stays unreadable.
+* **ASD 0x308 OOB read × HDCP.** Each TA runs at VA 0x400000 in its **own** address
+ space (`CONTRIBUTING.md`), so an ASD-space leak cannot read HDCP's guard.
+* **FINDING-1 (32 KB OOB write) × the PSR state machine.** The PSR work located real
+ state machine variables in CW6 (`ctx+0x730` psr_state, verified through GPINT). But
+ the host owns CW6 and can write it directly, so corrupting state via the overflow
+ buys nothing. This *lowers* FINDING-1's severity rather than raising it, and is worth
+ saying plainly.
+* **FINDING-8 × FINDING-9 (LSDMA).** Rollback accepts foreign images, so could a DCN4
+ image bring LSDMA to this card? No: `dcn_4_0_1` is rejected outright (0xffff0008) and
+ `dcn_3_5`, though accepted, never finishes boot on DCN 3.2 silicon.