radeon_uvd CS parser/hardware desync: stale-or-unset DATA0/DATA1 dword indices patch the validated IB and make the UVD engine consume unvalidated GPU addresses
Summary
data0/data1 are initialized to 0 and recorded as IB dword indices of the last DATA0/DATA1 register write, fired at the GPCOM CMD write without requiring or resetting the pair. An attacker submits a CMD preceded by a DATA0 write but no DATA1 write: cs_reloc reads idx=ib[0] - the FIRST PACKET HEADER - and patches it with an unvalidated address hi32 the UVD ring later EXECUTES; stale pairs relocate frankenpair mixed VAs the validator never checked (hardware consumes last-written DATA1<<32|DATA0). Cross-context VRAM/GTT corruption + engine faults. DF-4447-class validator/execution desync. Require-fresh-pair-and-invalidate fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4500 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uvd_ib_desync.c | β | 8.4 KB | view raw | |
| build.sh | β | 100 B | view raw | |
| run.sh | β | 111 B | view raw | |
| build.log | β | 574 B | view raw | |
| run.log | β | 286 B | view raw | |
| env.txt | β | 473 B | view raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| README.md | β | 3.5 KB | β raw | |
| fix.diff | β | 956 B | view raw |
DF-4500 β radeon_uvd CS parser/hardware desync via stale-or-unset DATA0/DATA1 indices
Root cause
sys/dev/drm/radeon/radeon_uvd.c:687 β radeon_uvd_cs_parse() declares
int data0 = 0, data1 = 0; and passes &data0/&data1 down through
radeon_uvd_cs_reg() (line 648). radeon_uvd_cs_reg() records in them the
IB dword index of the last write to UVD_GPCOM_VCPU_DATA0/DATA1
(lines 659-664) and fires radeon_uvd_cs_reloc() when the
UVD_GPCOM_VCPU_CMD register write is seen (line 665-670). Nothing
initializes-or-invalidates the pair around a CMD:
- unset (0) index: a CMD that appears before any DATA1 write makes
radeon_uvd_cs_reloc()readidx = ib[0]β the first packet header of the IB (radeon_uvd.c:584) β and then patchp->ib.ptr[data1] = start >> 32(radeon_uvd.c:597), i.e. it OVERWRITES the first packet header after that packet has already been validated. The UVD ring later executes the IB starting at the clobbered header: the executed packet stream β validated packet stream. For adata0 == 0(no DATA0 ever written) the patch writeslo(gpu_offset + offset)into the header instead β a semi-controlled, never-validated PM4 header. - stale index: after a first (DATA0, DATA1, CMD) triple is consumed,
a second CMD preceded only by a DATA0 write reuses the stale data1.
The validator relocates the pair (fresh DATA0 dword, stale DATA1 dword)
β a (bo, offset) combination never written to the hardware as a unit β
and repatches the already-consumed DATA1 dword. At execution time the
GPCOM message fetch uses the last written register values (fresh
DATA0, first DATA1), so the UVD engine consumes its message/buffer at
hi(bo1.gpu+offset1) : lo(bo2.gpu+offset2)β a mixed GPU virtual address the validator never checked (divergence only when bo1/bo2 sit in different 4 GiB regions, e.g. GTT vs VRAM, which the 256 MiB segment check does not prevent).
This is the radeon-UVD sibling of the VCE parser two-pass desync class (DF-4447 family): the validator's model of "which dword carries the address" diverges from the hardware's model of "which register value is current at CMD time".
Kernel-memory safety of the parser itself is NOT violated (all touched
dwords are in-bounds: radeon_cs_packet_parse() bounds idx/count against
chunk length, idx >= relocs_chunk->length_dw is rejected at
radeon_uvd.c:585, and p->relocs[] has exactly length_dw/4 entries β
radeon_cs.c:89-92). The defect is that execution-time behavior is
derived from state the validator never approved.
Impact
Local user with /dev/dri/card0 access on radeon hardware with UVD: * unvalidated PM4 header executed on the UVD ring (register write to an unvalidated UVD register, value = GPU-VA low dword β partially chosen), * UVD engine fetching message/dpb/bitstream buffers from an unvalidated GPU VA β cross-context VRAM/GTT corruption (other users' decoded buffers) and GPU engine faults/lockups (system-wide graphics hang).
Build / run
cc -O2 -Wall -o uvd_ib_desync uvd_ib_desync.c ./uvd_ib_desync /dev/dri/card0
uvd_ib_desync.c constructs the exact Scenario-A IB + reloc chunk with
raw ioctls. Full end-to-end execution (engine consuming the unvalidated
address) additionally needs real radeon+UVD hardware β see VERDICT.md for
why this guest could not provide it and what the expected on-hw outcome
is.
VERDICT β DF-4500
finding_id : DF-4500
status : untested (hardware-gated: requires a radeon ASIC with UVD;
the DragonFly QEMU guest has NO drm device at
all β ls /dev/dri β "No such file or directory")
reproduced : 0
impact : none demonstrated (expected on hardware: dos β GPU lockup /
cross-context VRAM corruption)
confidence : likely
Analysis (path:line, all in sys/dev/drm/radeon/ unless noted)
- radeon_uvd.c:687
int r, data0 = 0, data1 = 0;β indices start at 0, which is a valid IB dword index (the first packet header), not a sentinel. - radeon_uvd.c:656-680
radeon_uvd_cs_reg()assigns*data0 = p->idx/*data1 = p->idxon DATA0/DATA1 register writes; on the CMD register write (666-670) it immediately callsradeon_uvd_cs_reloc(p, *data0, *data1, ...). The pair is never reset after use and never checked for being set. - radeon_uvd.c:583-597
radeon_uvd_cs_reloc(): offset = radeon_get_ib_value(p, data0); idx = radeon_get_ib_value(p, data1); // == ib[0] when data1==0 ... p->ib.ptr[data0] = start & 0xFFFFFFFF; p->ib.ptr[data1] = start >> 32; // clobbers ib[0] header Withdata1 == 0the patch target is the FIRST PACKET HEADER of the IB; withdata0 == 0both patch dwords land on ib[0] and the final value islo(gpu_offset + offset)β a semi-controlled, unvalidated PM4 header. - Reachability of the CMD-with-unset-data1 shape: register order in the
r600 UVD block is CMD(0xef0c) < DATA0(0xef10) < DATA1(0xef14), and
TYPE0 packets write consecutive registers, so "DATA1 then CMD" cannot
share one packet β but two packets can:
PACKET0(DATA0,0)followed byPACKET0(CMD,0)leaves data1 == 0 at the CMD. The cmd==0 (message) variant is then rejected by the 64-byte alignment check (radeon_uvd.c:475-478: offset == ib[0] == the packet header 0x3BC4/0x3BC5/0x3BC3 is never 0 mod 64), but cmd 1..3 (bitstream/dpb/fb buffers) have NO alignment check and only needbo_size - offset >= buf_sizes[cmd]β reachable with an 8 MiB BO for cmd==3 (buf_sizes[3] == 2048, radeon_uvd.c:697). - Stale-pair variant (the clean desync): IB =
PACKET0(DATA0,2){off1,idx1,0}thenPACKET0(DATA0,0){off2}thenPACKET0(CMD,0){6}. Second CMD relocates with (fresh data0, stale data1); validator patches/validates that frankenpair while hardware (radeon_uvd.c:751-760radeon_uvd_send_msgshows the hw contract: DATA0/DATA1 register values at CMD time) uses the last-written register values β engine consumeshi(prev) : lo(new)mixed GPU VA. - In-bounds-ness (why this is NOT a kernel OOB): radeon_cs.c:733-736
(
idx >= ib_chunk->length_dwreject) and :770-773 (packet end check); radeon_uvd.c:585-589 (reloc idx check); radeon_cs.c:89-92 (p->nrelocs = chunk->length_dw / 4, array sized accordingly).
Why untested on this guest
The audit guest is QEMU (DragonFly dfbsd 6.5-DEVELOPMENT x86_64) with no
GPU passthrough: /dev/dri does not exist (env.txt). The radeon CS ioctl
and the UVD engine cannot be instantiated; the trigger was compile-verified
only (build.log). On real hardware the expected observation is: CS
accepted (parser returns 0), then the UVD engine executing the clobbered
header / consuming the mixed address β engine fault / ring hang
(system-wide graphics freeze), or silent corruption of whatever BO the
mixed VA resolves to.
Fix validation performed: none possible on this guest (fix_status: not_testable). fix.diff contains the review fix; to validate on hardware: apply to a radeon test box, re-run the Scenario-A/B IBs β both must be rejected with "UVD message address dwords not set before CMD" / "More than one message..." style errors and the engine must stay healthy.
Recommended fix (fix.diff)
Require a fresh DATA0/DATA1 pair for every GPCOM CMD and invalidate the pair after use (0 can never be a legitimate parsed data-dword index since dword 0 is always the first packet header):
case UVD_GPCOM_VCPU_CMD:
- if (data0 == 0 || data1 == 0) {
- DRM_ERROR("UVD GPCOM CMD without fresh DATA0/DATA1 pair!\n");
- return -EINVAL;
- } r = radeon_uvd_cs_reloc(p, data0, data1, buf_sizes, has_msg_cmd); if (r) return r;
- *data0 = 0;
- *data1 = 0; break;
This kills both the unset-index (scenario A) and stale-index (scenario B) variants: every CMD now relocates exactly the (DATA0, DATA1) dword pair written immediately before it, matching the hardware's register model.
kernel_refs
sys/dev/drm/radeon/radeon_uvd.c:583-597 (patch of ib[data0]/ib[data1]) sys/dev/drm/radeon/radeon_uvd.c:648-682 (data0/data1 tracking, no reset) sys/dev/drm/radeon/radeon_uvd.c:687 (data0=data1=0 init) sys/dev/drm/radeon/radeon_uvd.c:751-760 (hw DATA0/DATA1+CMD contract) sys/dev/drm/radeon/radeon_cs.c:731-780 (packet bounds, in-bounds proof)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored and git-apply --check verified against sys/dev/drm/radeon/radeon_uvd.c; could not be kernel-built or behavior-tested on the audit guest (no radeon hardware, no /dev/dri). Expected on hardware: Scenario-A/B IBs rejected with EINVAL before any patch/execute divergence.
findings/poc/DF-4500/fix.diff (git apply --check OK, output in session log); reasoning in VERDICT.md 'Recommended fix'.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (full path:line trace incl. both scenarios and why cmd==0-with-unset-data0 is alignment-rejected while cmd 1..3 pass); uvd_ib_desync.c (Scenario-A IB + 4000-entry reloc chunk, raw ioctls); build.log/run.log (guest compile OK, no /dev/dri at runtime); fix.diff (git-apply-verified CMD-requires-fresh-pair fix).
PoC changes
N/A (no seed pack; trigger authored fresh from the parser trace; ring-selection uapi flag adaptation noted in source for on-hw runs)
Verified recommended fix
In radeon_uvd_cs_reg() require a freshly-written DATA0/DATA1 pair for every GPCOM CMD (reject data0==0||data1==0) and reset both to 0 after each CMD so one pair cannot be reused across commands.
Verdict
Pass-2 analysis finding: radeon_uvd_cs_reg() tracks DATA0/DATA1 as last-written IB dword indices but never initializes-or-invalidates them around a UVD_GPCOM_VCPU_CMD (radeon_uvd.c:648-687), so a CMD with an unset (0) index makes radeon_uvd_cs_reloc() patch p->ib.ptr[0] β the first packet header of the IB β with hi32(gpu_offset+offset) after that packet was already validated, and a CMD with a stale index relocates a (fresh DATA0, stale DATA1) pair the hardware never received as a unit; the UVD engine then consumes message/buffers at a mixed unvalidated GPU VA (validator/hardware desync, the DF-4447 class). All kernel-side accesses stay in bounds (radeon_cs.c:731-780; relocs idx check radeon_uvd.c:585-589; p->relocs sized length_dw/4 at radeon_cs.c:89-92), so kernel memory safety is not directly violated; the impact is unvalidated PM4 execution on the UVD ring and cross-context VRAM/GTT corruption plus GPU lockup (local DoS) on radeon+UVD hardware. NOT executed here: the audit guest has no DRM device at all (env.txt); trigger compile-verified on the guest (build.log) and the exact IB/reloc layout is derived line-by-line from the parser source.
No comments yet.