diff options
| author | user <user@clank> | 2026-09-28 16:07:42 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-28 16:07:42 +0200 |
| commit | 553bc2fd0e1cef23bdeb133eb7b15125127cf46d (patch) | |
| tree | d116a532a75562af33d7ae2f6a17d85362f4e77f /dmcub | |
| parent | Scope the DMCUB findings by reachability, and test the user-influenced path (diff) | |
| download | amd-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.md | 69 | ||||
| -rw-r--r-- | dmcub/hw/dmcub_poc_dirtyrect.py | 40 | ||||
| -rw-r--r-- | dmcub/hw/dmcub_psr_bringup.py | 167 |
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()) |
