diff options
| author | user <user@clank> | 2026-09-28 16:19:54 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-28 16:19:54 +0200 |
| commit | 2bac54abd7ea6f9fbaba4cffe54c252cbf61fa97 (patch) | |
| tree | 6894e29da02ef53c7dc1f2d3bbf74c53e06c3bd7 | |
| parent | Reach the PSR-SU dirty-rect path by state injection; writes stay bounded (diff) | |
| download | amd-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.py | 122 | ||||
| -rw-r--r-- | psp_out/INDEX.md | 31 | ||||
| -rw-r--r-- | psp_out/SYNTHESIS-CROSS-FINDING-COMPOUNDS.md | 140 |
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. |
