aboutsummaryrefslogtreecommitdiffstats
path: root/dmcub
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-28 16:07:42 +0200
committeruser <user@clank>2026-09-28 16:07:42 +0200
commit553bc2fd0e1cef23bdeb133eb7b15125127cf46d (patch)
treed116a532a75562af33d7ae2f6a17d85362f4e77f /dmcub
parentScope the DMCUB findings by reachability, and test the user-influenced path (diff)
downloadamd-psp-re-553bc2fd0e1cef23bdeb133eb7b15125127cf46d.tar.gz
amd-psp-re-553bc2fd0e1cef23bdeb133eb7b15125127cf46d.tar.xz
amd-psp-re-553bc2fd0e1cef23bdeb133eb7b15125127cf46d.zip
Reach the PSR-SU dirty-rect path by state injection; writes stay bounded
The earlier user-reachability test was weak because the DMCUB was idle. PSR is an eDP feature and this is a desktop card, so a real panel session is not available. Driving the firmware's own ABI first, and reporting that it failed: PSR_SET_VERSION -> COPY_SETTINGS -> ENABLE are all consumed but the state never leaves 0, read back through GPINT 7 with GET_FW_VERSION (0x07003300) as the oracle's positive control. Command injection alone does not open a session without a link. The state word is instead located at ctx + 0x7b8*panel + 0x730 -- the address the GPINT handler reports from, and the value the dirty-rect handler compares against 0x30/0x31/0x44/0x60 -- and injected in host-owned CW6. Three write/read-back round trips through the firmware's own reporting path confirm it takes effect, so the session is injected but not merely assumed. With PSR_STATE3 set the same nine user-reachable coordinate cases produce much larger state writes (4-8 bytes idle, 20-122 bytes now), proving the deeper path executes. Dumping where the bytes change rather than how many shows the offsets never move with the input: two fixed 48-byte arrays at ctx+0x2cc and ctx+0x3ec, which are exactly 3 rects x 16 bytes, plus small fixed fields, all inside the fw_state window. No user-reachable OOB write. Caveat kept: the trace buffer gained no entries and no DCN register programming was monitored, so the final region->register stage, which needs a live OTG, probably still does not run.
Diffstat (limited to 'dmcub')
-rw-r--r--dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md69
-rw-r--r--dmcub/hw/dmcub_poc_dirtyrect.py40
-rw-r--r--dmcub/hw/dmcub_psr_bringup.py167
3 files changed, 266 insertions, 10 deletions
diff --git a/dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md b/dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md
index 1cc67c5..b90d30d 100644
--- a/dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md
+++ b/dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md
@@ -91,13 +91,72 @@ cmd_version 2 yes True 4
input either — the `x2<x1` and `count=3` cases change 8 bytes where the others change 4,
so the response is input-dependent.
+## Reaching the PSR-SU path: state injection, verified by the firmware itself
+
+The result above was weak because the DMCUB was idle. PSR is an eDP feature and this is
+a desktop card, so a genuine panel session is unavailable. Two things made the path
+reachable anyway.
+
+**First, the firmware's own ABI was tried and honestly failed.** `hw/dmcub_psr_bringup.py`
+sends `PSR_SET_VERSION(SU_1)` → `PSR_COPY_SETTINGS` → `PSR_ENABLE` exactly as
+`dmub_psr_copy_settings()`/`dmub_psr_enable()` do, reading the state after each step via
+**GPINT 7 (GET_PSR_STATE)**, with `GET_FW_VERSION` = `0x07003300` as the oracle's
+positive control. All three commands were consumed and the state never left `0x0`:
+command injection alone does not open a session without a real link.
+
+**Second, the state word was located and injected directly.** The GPINT handler reads
+the per-panel state from `ctx + 0x7b8*panel + 0x730`, and the dirty-rect handler at
+`0x600330e4` compares that value against `0x30/0x31/0x44/0x60` — the `PSR_STATE*` enum.
+Writing the word in host-owned CW6 and then asking the *firmware* to report it closes
+the loop:
+
+```
+wrote 0x30 at ctx+0x730 -> GPINT GET_PSR_STATE = 0x00000030 MATCH
+wrote 0x31 at ctx+0x730 -> GPINT GET_PSR_STATE = 0x00000031 MATCH
+wrote 0x00 at ctx+0x730 -> GPINT GET_PSR_STATE = 0x00000000 MATCH
+```
+
+This is state injection, not a real session — but it is verified through the firmware's
+own reporting path rather than assumed.
+
+## Re-test with the PSR-SU path actually executing
+
+With `PSR_STATE3` injected, the same nine user-reachable cases behave visibly
+differently, which is the proof the deeper path now runs:
+
+```
+case consumed alive CW6 diff (idle -> PSR_STATE3)
+benign 64x64 yes True 4 -> 20
+height = 0xffffffff yes True 4 -> 32
+x2<x1 (width = -16) yes True 8 -> 47
+all three hostile yes True 4 -> 122
+count=3, mixed yes True 8 -> 83
+```
+
+**But the writes are bounded.** Dumping *where* the bytes change, rather than how many,
+shows the same fixed regions in every case regardless of input:
+
+```
+all three hostile: ctx+0xcc (1B) ctx+0x2cc (48B) ctx+0x3ec (48B)
+ ctx+0x50c (16B) ctx+0x570 (4B) ctx+0x578 (4B) ctx+0x588 (4B)
+x,y = 0xffffffff: ctx+0xcc (2B) ctx+0x2d4 (40B) ctx+0x3f4 (40B) ...
+benign: ctx+0x3f4 (8B) ctx+0x514 (8B) ctx+0x578 (4B) ...
+```
+
+`0x2cc` and `0x3ec` are **48 bytes each = 3 rects x 16 bytes**: the handler copies the
+rectangles into two fixed-size state arrays. The offsets never move with the input, only
+the contents; every write stays inside the `0xb8c0` fw_state window. The larger diffs for
+hostile input are simply more bytes *differing* within those same arrays.
+
+**Verdict: no user-reachable OOB write, with the PSR-SU path demonstrably executing.**
+
## Limits — why this is not "type 67 is safe"
-* The DMCUB here is **idle with no panel and no real PSR session**. Planting the gate
- byte gets past the shim, but the deeper PSR-SU region programming almost certainly
- bails earlier than it would on a live display, so the most interesting arithmetic
- (region coordinates → register programming) may never have executed. This is the same
- limitation that blocks FINDING-4.
+* The PSR session is **injected, not real**. The rect-storage stage demonstrably runs
+ (above), but the trace buffer shows **zero** new entries and no DCN register
+ programming was monitored, so the final region→register stage — which needs a live
+ OTG and stream — probably still does not execute. That last stage is where coordinate
+ arithmetic would turn into hardware programming, and it remains untested.
* The oracle is ring progress plus a CW6 diff. A write *outside* CW6, or a corruption
that only manifests on the next frame, would not be seen.
* `UPDATE_CURSOR_INFO` (type 68) is **not** tested here, only shown to be
diff --git a/dmcub/hw/dmcub_poc_dirtyrect.py b/dmcub/hw/dmcub_poc_dirtyrect.py
index c1beb2e..a95b810 100644
--- a/dmcub/hw/dmcub_poc_dirtyrect.py
+++ b/dmcub/hw/dmcub_poc_dirtyrect.py
@@ -89,6 +89,9 @@ def main():
ap = argparse.ArgumentParser()
ap.add_argument('--state')
ap.add_argument('--apply', action='store_true')
+ ap.add_argument('--psr-state', type=lambda x: int(x, 0), default=None,
+ help='inject a PSR state (e.g. 0x30 = PSR_STATE3) at ctx+0x730 so '
+ 'the handler takes the PSR-SU path; verified via GPINT 7')
ap.add_argument('--image', default=os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'fw', 'dcn_3_2_0_dmcub.bin'))
@@ -109,6 +112,31 @@ def main():
def snap():
return bytes(adev.vram.view(fw_paddr, plan.fw_region_size & ~3)[:])
+ trace_paddr = int(st['paddr']['tracebuff'])
+
+ def tsnap():
+ return bytes(adev.vram.view(trace_paddr, 0x4000)[:])
+
+ if args.psr_state is not None:
+ import time
+ off = (ctx - B.DMUB_CW6_BASE) + 0x730 # per-panel PSR state word
+ adev.vram.view(fw_paddr + off, 4)[:] = struct.pack('<I', args.psr_state)
+ adev.gmc.flush_hdp()
+ # verify through the firmware's own reporting path, not just our write
+ val = (1 << 28) | (7 << 16)
+ regs.write('DMCUB_GPINT_DATAIN1', val)
+ t0 = time.time()
+ got = None
+ while time.time() - t0 < 0.3:
+ if regs.read('DMCUB_GPINT_DATAIN1') == (val & ~(0xf << 28)):
+ got = regs.read('DMCUB_SCRATCH7')
+ break
+ print('injected PSR state 0x%x; firmware reports 0x%s via GPINT 7 -> %s' % (
+ args.psr_state, ('%08x' % got) if got is not None else '??',
+ 'ACTIVE' if got == args.psr_state else 'NOT REFLECTED'))
+ if got != args.psr_state:
+ raise SystemExit('state not reflected; results would be meaningless')
+
# plant the PSR state byte the shim gates on (CW6 is host-owned)
for inst in (0, 1):
off = (ctx - B.DMUB_CW6_BASE) + GATE_STRIDE * inst + GATE_OFF
@@ -119,17 +147,19 @@ def main():
adev.gmc.flush_hdp()
print('planted the PSR gate byte at ctx+0x%x (both panels)' % GATE_OFF)
- print('\n%-24s %-9s %-8s %s' % ('case', 'consumed', 'alive', 'CW6 bytes changed'))
+ print('\n%-24s %-9s %-8s %-8s %s' % (
+ 'case', 'consumed', 'alive', 'CW6 diff', 'trace diff'))
stalled = None
for label, rects, count in CASES:
- before = snap()
+ before, tbefore = snap(), tsnap()
ver = 2 if 'version 2' in label else 1
reply = poc.submit(frame(rects, count, cmd_version=ver))
- after = snap()
+ after, tafter = snap(), tsnap()
diff = sum(1 for a, b in zip(before, after) if a != b)
+ tdiff = sum(1 for a, b in zip(tbefore, tafter) if a != b)
alive = poc.alive()
- print(' %-22s %-9s %-8s %d' % (
- label, 'yes' if reply is not None else 'NO', alive, diff))
+ print(' %-22s %-9s %-8s %-8d %d' % (
+ label, 'yes' if reply is not None else 'NO', alive, diff, tdiff))
if reply is None or not alive:
stalled = label
break
diff --git a/dmcub/hw/dmcub_psr_bringup.py b/dmcub/hw/dmcub_psr_bringup.py
new file mode 100644
index 0000000..05067fa
--- /dev/null
+++ b/dmcub/hw/dmcub_psr_bringup.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+"""Drive DMCUB's PSR state machine far enough to reach the PSR-SU dirty-rect path.
+
+Motivation (`../AUDIT-DMCUB-06-USER-REACHABLE.md`): the only user-influenced DMCUB
+surface is the dirty-rect / cursor / PSR family, and hostile coordinates survived a
+hardware test -- but on an *idle* DMCUB, where the deep PSR-SU region programming
+almost certainly never ran. PSR is an eDP feature and this is a desktop card with no
+eDP panel, so a genuine panel session is not available. What *is* available:
+
+ * we drive the inbox ourselves, so we can send DMUB_CMD__PSR (type 64) sub-commands
+ exactly as amdgpu's dmub_psr_copy_settings()/dmub_psr_enable() do;
+ * GPINT command 7 (GET_PSR_STATE) reports the firmware's own state, so progress is
+ *measured* rather than assumed. GPINT is validated first with GET_FW_VERSION,
+ which must return 0x07003300 (7.0.51) on this card.
+
+Sequence, with the state read after every step:
+ PSR_SET_VERSION(SU_1) -> PSR_COPY_SETTINGS -> PSR_ENABLE
+
+Frame layouts from dmub_cmd.h, data at frame+4:
+ set_version: +0x00 version(u32) +0x04 cmd_version +0x05 panel_inst
+ copy_settings: +0x04 psr_level +0x06 dpp +0x08 opp +0x09 otg +0x0a digfe
+ +0x0b digbe +0x0d aux +0x10 frame_cap_ind +0x11 su_y_granularity
+ +0x1e cmd_version +0x1f panel_inst
+ enable: header sub_type carries enable/disable; panel_inst in the data
+
+This is state injection through the firmware's own ABI, not a real link. If the state
+does not advance, that is reported as such -- a PSR session that only exists in our
+heads would invalidate any dirty-rect result built on top of it.
+
+usage: dmcub_psr_bringup.py --state state.json --apply
+"""
+import argparse
+import json
+import os
+import struct
+import sys
+import time
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import dmcub_hwregs as H # noqa: E402
+import dmcub_bringup as B # noqa: E402
+from dmcub_poc_type8 import Poc, ctx_from_image, RB_CMD_SIZE # noqa: E402
+
+DMUB_CMD__PSR = 64
+PSR_SET_VERSION, PSR_COPY_SETTINGS, PSR_ENABLE, PSR_DISABLE = 0, 1, 2, 3
+PSR_VERSION_SU_1 = 1
+PSR_VERSION_1 = 0
+
+
+def hdr(sub, payload=0x38):
+ f = bytearray(RB_CMD_SIZE)
+ f[0] = DMUB_CMD__PSR
+ f[1] = sub
+ f[3] = payload
+ return f
+
+
+def f_set_version(version, panel_inst=0, cmd_version=0):
+ f = hdr(PSR_SET_VERSION)
+ struct.pack_into('<I', f, 4, version)
+ f[4 + 4] = cmd_version
+ f[4 + 5] = panel_inst
+ return bytes(f)
+
+
+def f_copy_settings(panel_inst=0, otg=0, digfe=0, digbe=0, cmd_version=0, su_gran=4):
+ f = hdr(PSR_COPY_SETTINGS)
+ d = 4
+ struct.pack_into('<H', f, d + 0x04, 0) # psr_level
+ f[d + 0x06] = 0 # dpp_inst
+ f[d + 0x08] = 0 # opp_inst
+ f[d + 0x09] = otg
+ f[d + 0x0a] = digfe
+ f[d + 0x0b] = digbe
+ f[d + 0x0d] = 0 # aux_inst
+ f[d + 0x10] = 1 # frame_cap_ind
+ f[d + 0x11] = su_gran
+ struct.pack_into('<I', f, d + 0x18, 10) # line_time_in_us
+ f[d + 0x1e] = cmd_version
+ f[d + 0x1f] = panel_inst
+ return bytes(f)
+
+
+def f_enable(enable, panel_inst=0):
+ f = hdr(PSR_ENABLE if enable else PSR_DISABLE)
+ f[4 + 0] = panel_inst
+ f[4 + 1] = 0
+ return bytes(f)
+
+
+class Gpint:
+ def __init__(self, regs):
+ self.r = regs
+
+ def __call__(self, cmd, param=0, timeout=0.3):
+ val = (1 << 28) | ((cmd & 0xfff) << 16) | (param & 0xffff)
+ self.r.write('DMCUB_GPINT_DATAIN1', val)
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ if self.r.read('DMCUB_GPINT_DATAIN1') == (val & ~(0xf << 28)):
+ return self.r.read('DMCUB_SCRATCH7')
+ return None
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--state', required=True)
+ ap.add_argument('--apply', action='store_true')
+ ap.add_argument('--panel', type=int, default=0)
+ ap.add_argument('--version', type=int, default=PSR_VERSION_SU_1)
+ ap.add_argument('--image', default=os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ 'fw', 'dcn_3_2_0_dmcub.bin'))
+ args = ap.parse_args()
+ st = json.load(open(args.state))
+
+ from tinygrad import Device
+ adev = Device["AMD"].iface.dev_impl
+ regs = H.Regs(adev=adev, inst=0, dry_run=not args.apply)
+ if regs.validate(verbose=False)[0] == 'none':
+ raise SystemExit('no register path')
+ g = Gpint(regs)
+
+ fw = g(1)
+ print('GPINT control: GET_FW_VERSION = %s' % (
+ '0x%08x' % fw if fw is not None else 'NO ACK'))
+ if fw != 0x07003300:
+ raise SystemExit('GPINT oracle not trustworthy; aborting')
+
+ plan = B.Plan(args.image)
+ poc = Poc(regs, adev, int(st['paddr']['fw_state']), int(st['paddr']['mailbox']),
+ ctx_from_image(args.image), plan.fw_region_size, dry_run=not args.apply)
+
+ def state(tag):
+ s = g(7, args.panel)
+ print(' %-26s PSR state = %s' % (
+ tag, '0x%08x' % s if s is not None else 'NO ACK'))
+ return s
+
+ print('\ndriving the PSR state machine on panel %d:' % args.panel)
+ base = state('baseline')
+ steps = [('SET_VERSION(%d)' % args.version, f_set_version(args.version, args.panel)),
+ ('COPY_SETTINGS', f_copy_settings(args.panel)),
+ ('ENABLE', f_enable(True, args.panel))]
+ results = {}
+ for label, fr in steps:
+ consumed = poc.submit(fr) is not None
+ s = state('after %s (consumed=%s)' % (label, consumed))
+ results[label] = (consumed, s)
+
+ print('\n=== result')
+ final = results.get('ENABLE', (None, None))[1]
+ if final not in (None, base):
+ print(' PSR state advanced %s -> %s: the firmware accepted the session, so the'
+ % ('0x%x' % base, '0x%x' % final))
+ print(' PSR-SU dirty-rect path is now worth re-testing with hostile coordinates.')
+ else:
+ print(' PSR state never left 0x%x.' % (base or 0))
+ print(' The firmware did not enter a PSR session from command injection alone;')
+ print(' without a real eDP link the deeper SU path stays unreachable here, and')
+ print(' any dirty-rect result must keep that caveat.')
+ print(' dmcub alive: scratch0=0x%x' % regs.read('DMCUB_SCRATCH0'))
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())