β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4500

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4500 Β· 9 files
FileTypeDescriptionSize
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:

  1. unset (0) index: a CMD that appears before any DATA1 write makes radeon_uvd_cs_reloc() read idx = ib[0] β€” the first packet header of the IB (radeon_uvd.c:584) β€” and then patch p->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 a data0 == 0 (no DATA0 ever written) the patch writes lo(gpu_offset + offset) into the header instead β€” a semi-controlled, never-validated PM4 header.
  2. 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.md
↓ download raw

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)

  1. 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.
  2. radeon_uvd.c:656-680 radeon_uvd_cs_reg() assigns *data0 = p->idx / *data1 = p->idx on DATA0/DATA1 register writes; on the CMD register write (666-670) it immediately calls radeon_uvd_cs_reloc(p, *data0, *data1, ...). The pair is never reset after use and never checked for being set.
  3. 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 With data1 == 0 the patch target is the FIRST PACKET HEADER of the IB; with data0 == 0 both patch dwords land on ib[0] and the final value is lo(gpu_offset + offset) β€” a semi-controlled, unvalidated PM4 header.
  4. 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 by PACKET0(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 need bo_size - offset >= buf_sizes[cmd] β€” reachable with an 8 MiB BO for cmd==3 (buf_sizes[3] == 2048, radeon_uvd.c:697).
  5. Stale-pair variant (the clean desync): IB = PACKET0(DATA0,2){off1,idx1,0} then PACKET0(DATA0,0){off2} then PACKET0(CMD,0){6}. Second CMD relocates with (fresh data0, stale data1); validator patches/validates that frankenpair while hardware (radeon_uvd.c:751-760 radeon_uvd_send_msg shows the hw contract: DATA0/DATA1 register values at CMD time) uses the last-written register values β†’ engine consumes hi(prev) : lo(new) mixed GPU VA.
  6. In-bounds-ness (why this is NOT a kernel OOB): radeon_cs.c:733-736 (idx >= ib_chunk->length_dw reject) 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.

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_testable
baseline no→ patch + rebuild →patched clean

fix.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'.
↓ fix.diffper-fix-DF-4500

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.