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

uvd_v4_2_resume programs the VCPU cache-window chain +0x200 into vcpu_bo with a full-blob region0 size β€” chain ends 512 bytes past the pinned bo with the shipping CIK UVD firmware (radeon twin of DF-4832/DF-4524/DF-4742)

Summary

With fw_header_present (live path for BONAIRE/KABINI/KAVERI/HAWAII/MULLINS), region0 starts gpu_addr+0x200 but is sized RADEON_GPU_PAGE_ALIGN(datasize+4) for the FULL blob while the bo is sized ALIGN(datasize+8)+HEAP+STACK+SESSION*N - the contiguous region chain ends bo_end+512 for 99.90% of fw sizes; shipping bonaire_uvd datasize=232752 gives exactly +512B, mapping the SESSION tail over the head of the adjacent VRAM GEM object (GPU-scope cross-object read/write, unpriv decode CS). Arithmetic-proven exhaustively (65472/65536 sizes, min=max=512B, shipping fw decoded from in-tree .uu); hardware-gated acceptance basis same as DF-4524/4742/4832. offset+size==fw-region invariant fix (amdgpu precedent amdgpu_uvd.h:32-38).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5229 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 6.0 KB ↓ raw
check_arith.py β€” 5.3 KB view raw
arith.log β€” 949 B view raw
decode_fw.py β€” 1.5 KB view raw
fw_header.txt β€” 885 B view raw
bonaire_uvd.bin β€” 227.3 KB ↓ download
build.sh β€” 135 B view raw
run.sh β€” 168 B view raw
env.txt β€” 25 B view raw
fix.diff β€” 872 B view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.3 KB view raw

DF-5229 β€” radeon UVD v4.2 VCPU cache-window chain overruns vcpu_bo by 512 bytes (shipping CIK firmware)

File: sys/dev/drm/radeon/uvd_v4_2.c:46-64 (vs bo sizing sys/dev/drm/radeon/radeon_uvd.c:184-186) Severity: Low (GPU-scope VRAM overhang only) Β· Bucket: memcorrupt Family: radeon twin of DF-4832 (uvd_v1_0, 128 B); amdgpu cousins DF-4524 (vce_v3_0, 256 B) / DF-4742 (vce_v2_0, 256 B)

What to run

./build.sh        # host: decode the in-tree shipping fw -> fw_header.txt
python3 check_arith.py   # -> arith.log (exhaustive sweep; no kernel/hw needed)

Expected output (decisive lines, arith.log)

header path: overrun bytes: min=512 max=512 ; vulnerable sizes: 65472/65536 (99.90%)
safe residues (ds%4096): [4089, 4090, 4091, 4092]
control: headerless path ... overrun sizes: 0/65536
datasize=232752 (v1.64 fw -> N=30): bo_size=2236416 span=2236928 overrun=512 bytes
  last window tail = gpu_addr + 0x222200 vs bo end gpu_addr + 0x222000
post-fix span == bo_size (header path): True

The shipping CIK UVD firmware (sys/contrib/dev/drm/radeonfw/bonaire_uvd.bin.uu, loaded as radeonkmsfw_bonaire_uvd β€” the preferred fw for BONAIRE/KABINI/KAVERI/HAWAII/MULLINS, radeon_uvd.c:124-131,139-141) decodes as datasize=232752, %4096=3376 (fw_header.txt), so ALIGN(ds+4)==ALIGN(ds+8) and the STACK/SESSION window programmed by uvd_v4_2_resume ends exactly 512 bytes past the end of rdev->uvd.vcpu_bo β€” inside the VRAM object allocated after it.

Reproduce on hardware (not possible here)

  1. Bare metal / GPU-passthrough VM with a CIK UVD GPU (BONAIRE, KABINI, KAVERI, HAWAII, MULLINS β€” radeonkms driver, new-format UVD fw loaded).
  2. Trigger UVD use (any VDPAU/video decode CS on the UVD ring) β€” windows are programmed at ring start (cik.c:8175) or DPM ungate (kv_dpm.c:1628).
  3. The last 512 B of the SESSION region (tail of handle 30's session) are GPU-written into the adjacent VRAM object β€” cross-object GPU read/write between GEM bo's (e.g. another user's buffer).

Control & fix

  • Headerless control (uvd_v4_2.c:49 +0 / ALIGN(ds+4)): 0 overruns for all 65536 sizes β€” identical to the accepted-clean uvd_v2_2 shape.
  • fix.diff: shrink region0 by the 0x200 skip so offset0+size0 equals the fw region the bo actually holds (ALIGN(datasize+8)) β€” the same invariant amdgpu uses (AMDGPU_UVD_FIRMWARE_OFFSET + AMDGPU_UVD_FIRMWARE_SIZE == ALIGN(ucode_size+8), sys/dev/drm/amd/amdgpu/amdgpu_uvd.h:32-38). Post-fix span == bo_size for every ds in [0,0x2FFFF] and both N (arith.log).
VERDICT.md
↓ download raw

DF-5229 VERDICT β€” uvd_v4_2.c 512-byte VCPU-cache window-chain overhang

Status: untested (hardware-gated) β€” arithmetic overrun certain with the shipping firmware (not just generic residues); runtime effect fw-behavior-dependent. Impact: GPU-scope VRAM overhang of 512 bytes past a pinned bo. Confidence: speculative (mirrors accepted DF-4524/DF-4742/DF-4832; stronger evidence than DF-4832: the in-tree blob's exact size is known).

The defect

radeon_uvd_init sizes the shared UVD firmware bo once for every generation (radeon_uvd.c:184-186):

bo_size = RADEON_GPU_PAGE_ALIGN(uvd_fw->datasize + 8)
        + RADEON_UVD_STACK_SIZE + RADEON_UVD_HEAP_SIZE
        + RADEON_UVD_SESSION_SIZE * max_handles      /* radeon.h:1675-1677 */

uvd_v4_2_resume β€” the CIK UVD entry (cik.c:8175, kv_dpm.c:1628) β€” programs the VCPU LMI cache windows starting 512 bytes into the bo when the new-format fw is present, but keeps a full-blob region0 size (uvd_v4_2.c:46-53):

addr = (gpu_addr + 0x200) >> 3;                     /* +512 B    :46-47  */
size = RADEON_GPU_PAGE_ALIGN(datasize + 4) >> 3;     /* full blob :51     */
addr += size; size = HEAP >> 3;                      /*           :55-58  */
addr += size; size = (STACK + SESSION*max_handles) >> 3;   /*    :60-64  */

The windows are laid out contiguously, so the chain end is 0x200 + ALIGN(ds+4) + HEAP + STACK + SESSION*N from the bo base, while the bo ends at ALIGN(ds+8) + HEAP + STACK + SESSION*N:

  • ds%4096 ∈ [0,4088] βˆͺ [4093,4095] (4092/4096 = 99.90% of sizes) β†’ ALIGN(ds+4) == ALIGN(ds+8) β†’ chain ends bo_end + 512.
  • ds%4096 ∈ [4089,4092] β†’ the +8 wins a page β†’ fits (3.5 KiB slack).

With the shipping firmware this is not a residue argument: sys/contrib/dev/drm/radeonfw/bonaire_uvd.bin.uu decodes to datasize=232752 (0x38d30), size_bytes=232752 (radeon_ucode_validate passes, radeon_uvd.c:161), ucode_version 1.64 β‰₯ 1.37 β†’ max_handles=30 (radeon_uvd.c:165-166), and datasize%4096=3376 β†’ overhang exactly 512 B (bo=0x222000, span=0x222200). The fw is the preferred load for every CIK UVD ASIC (radeon_uvd.c:124-131 sets fw_header_present=true at :153), so the vulnerable branch is the live path whenever that kmod is loaded.

Overhang lands on the STACK/SESSION window tail = per-session VCPU memory (50 KiB/handle Γ— 30 handles) β†’ the last 512 bytes of handle-30's session are GPU-read/written inside the next VRAM object allocated after vcpu_bo.

Controls and divergence

  • Headerless branch (uvd_v4_2.c:49): +0 offset, ALIGN(ds+4) β†’ fits for all 65536 sizes (arith.log) β€” same shape as accepted-clean uvd_v2_2 (uvd_v2_2.c:113-126).
  • DF-4832 (uvd_v1_0) is the 128 B headerless twin; DF-4524/DF-4742 are the amdgpu VCE 256 B twins. All accepted on arithmetic proof alone.
  • amdgpu fixed this class for the same silicon: AMDGPU_UVD_FIRMWARE_OFFSET (256) + AMDGPU_UVD_FIRMWARE_SIZE(adev) (= ALIGN(ucode_size_bytes+8) βˆ’ 256) (amdgpu_uvd.h:32-38) makes offset+size exactly the aligned fw region; radeon's uvd_v4_2 (+0x200 skip with full-ALIGN(datasize+4) size) never got the equivalent. Note the blob's real payload offset is 0x100 (ucode_array_offset_bytes=256, fw_header.txt) β€” the 0x200 entry skip is the historical (upstream-Linux verbatim) value; the fix deliberately does not change the entry offset (fw-behavior validated by a decade of hardware use), only the region sizes.

Why it cannot be runtime-verified here

Guest has no AMD ASIC at all (env.txt: NO_AMD_PCI, no drm kmods) β€” needs a BONAIRE/KAVERI/KABINI/MULLINS/HAWAII GPU + new-format fw + a CS decode session so the VCPU actually touches the window tail. Identical situation to the accepted twins DF-4524/DF-4742/DF-4832.

Threat model

Unprivileged local user on CIK UVD hardware who can submit CS to the UVD ring (video decode) β†’ the UVD VCPU's session/stack tail maps over the first 512 bytes of the next VRAM object. Ceiling: GPU-side cross-object info-leak/corruption between adjacent GEM bo's (e.g. another user's buffer) β€” not kernel host memory: the LMI windows map VRAM; past VRAM there is no valid target (hang/reset, not host corruption). Nothing in uvd_v4_2.c consumes user-controlled data; all inputs are kernel-fixed constants + fw size.

Fix validation (arithmetic-exact; not kernel-runnable here)

fix.diff: when fw_header_present, program size0 = (RADEON_GPU_PAGE_ALIGN(datasize + 8) βˆ’ 0x200) >> 3 so that offset0+size0 == ALIGN(datasize+8) (the fw region the bo actually holds) β€” region1/2 shift 512 B earlier within the same bo and the chain ends exactly at the bo boundary. Harness tail proves: post-fix span == bo_size for every ds in [0,0x2FFFF] sampled every 7 bytes (both N=10 and N=30), and the headerless branch is untouched. git apply --check clean.

Negative-check list (everything else hunted in uvd_v4_2.c)

  • NULL uvd_fw deref via the kv DPM ungate caller β€” REAL, verified with a guest panic + fix validation, filed as DF-5230.
  • Guarded caller path (cik.c:8167 has_uvd; radeon_uvd.c:286 vcpu_bo) β€” safe.
  • Boot-time gate path (kv_dpm_late_enable β†’ uvd_v1_0_stop) β€” register writes only (uvd_v1_0.c:391-411) β€” safe.
  • 64β†’32-bit truncations: OFFSET0/SIZE0 regs hold addr>>3 by design; bits 28-31 at :67 and 32-39 at :71 carry the high bits β€” correct.
  • max_handles (≀30) Γ— SESSION fits the handles[] arrays (radeon.h:1685) β€” and SESSION*N cancels between span and bo (arith.log N-independence) β€” no independent overflow.
  • This file has no ring/emitter/cntl overrides (v4_2 is resume-only; ring funcs come from uvd_v1_0.c via cik.c:8208 r600_ring_init / uvd_v1_0_init) β€” nothing else to audit in it.

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 not kernel-validated: no CIK UVD GPU on this guest, no baseline to flip. fix.diff is arithmetic-exact: post-fix span == bo_size for every ds in [0,0x2FFFF] (sampled every 7 bytes) and both max_handles values; headerless branch untouched; git apply --check clean.

fix.diff (git apply --check clean; window chain ends exactly at bo end); arith.log tail (fix-validation sweep)
↓ fix.diffper-fix-DF-5229

Confirmed kernel references

Detail

Evidence (decisive lines)

arith.log (exhaustive: min=max=512B, 65472/65536 sizes vulnerable, safe residues exactly [4089..4092]; headerless control 0 overruns; shipping fw bo=0x222000 span=0x222200; post-fix span==bo_size for all sampled ds and both N); fw_header.txt (decoded shipping blob: datasize 232752, size_bytes==datasize so radeon_ucode_validate passes, ucode 1.64 -> N=30, payload offset 0x100); VERDICT.md (derivation + amdgpu divergence + negative checks); env.txt (guest: NO_AMD_PCI)

PoC changes

no seed; arithmetic harness written fresh DF-4832-style (its check_arith.py already modeled v4_2_span and cross-noted the 512B shape); added the exact shipping-firmware constants decoded from the in-tree .uu blob

Verified recommended fix

size region0 so offset0+size0 == the fw region the bo holds: when fw_header_present, size = (RADEON_GPU_PAGE_ALIGN(datasize+8) - 0x200) >> 3 (amdgpu invariant), keeping the whole window chain inside vcpu_bo

Verdict

uvd_v4_2_resume (sys/dev/drm/radeon/uvd_v4_2.c:46-64) programs the UVD VCPU LMI cache windows starting 512 bytes into vcpu_bo when fw_header_present (the live path for BONAIRE/KABINI/KAVERI/HAWAII/MULLINS with the preferred new-format fw, radeon_uvd.c:124-131,153) but keeps region0 sized RADEON_GPU_PAGE_ALIGN(datasize+4) for the FULL blob, while radeon_uvd.c:184-186 sizes the bo at ALIGN(datasize+8)+HEAP+STACK+SESSIONN. The contiguous window chain therefore ends at 0x200+ALIGN(ds+4)+HEAP+STACK+SESSIONN = bo_end+512 for 99.90% of fw sizes (safe only for ds%4096 in [4089..4092]). With the SHIPPING firmware (in-tree sys/contrib/dev/drm/radeonfw/bonaire_uvd.bin.uu: datasize=232752, %4096=3376, ucode 1.64 -> max_handles=30) the overhang is exactly +512 bytes (bo=0x222000, span=0x222200): the tail of the 30-handle SESSION region is GPU-mapped into the VRAM object allocated after vcpu_bo. Proven exhaustively by check_arith.py (arith.log; headerless control 0/65536 overruns, N-independence shown). Not runtime-verifiable: guest has no AMD ASIC (env.txt) and the window-tail access is fw-behavior-dependent during a real decode session -- same acceptance basis as DF-4524/DF-4742/DF-4832 (DF-4832's harness already cross-noted this exact shape). amdgpu fixed the class for the same silicon via the offset+size==fw-region invariant (amdgpu_uvd.h:32-38); radeon never got it. GPU-scope only: LMI windows map VRAM, not host memory; ceiling is 512B cross-object GPU read/write between adjacent GEM bo's during video decode (CS-reachable by unprivileged users on real hw). No kernel host-memory impact; no user-controlled input in this file.