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

uvd_v1_0_resume programs VCPU cache windows +128B into vcpu_bo at full sizes β€” STACK/SESSION window overruns the pinned bo by 128 bytes into the adjacent VRAM object (radeon UVD v1 twin of DF-4524/DF-4742)

Summary

The window base jumps +128B into the bo but the sizes were never shrunk - for 99.8% of fw sizes (ALIGN(datasize+8)==ALIGN(datasize)) the STACK/SESSION window ends exactly 128B past the bo, inside the adjacent VRAM object. Exhaustive mod-4096 arithmetic proof (control uvd_v2_2 clean for all residues). GPU-scope only (LMI maps VRAM, not host memory): unpriv CS-driven decode can cross-object read/write 128B of the next GEM bo on R600/RV770-class hw. Shrink-fw-window-by-16-units fix, arithmetic-exact for every residue. Cross-noted: uvd_v4_2.c:47 shows a 512B-class overhang shape (pass-1 clean; follow-up).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4832 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 5.6 KB ↓ raw
check_arith.py β€” 4.4 KB view raw
arith.log β€” 1.3 KB view raw
env.txt β€” 483 B view raw
fix.diff β€” 824 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.5 KB view raw

DF-4832 β€” radeon UVD v1.0 VCPU cache windows overrun vcpu_bo by 128 bytes

File: sys/dev/drm/radeon/uvd_v1_0.c:121-135 (vs bo sizing in sys/dev/drm/radeon/radeon_uvd.c:184-186) Severity: Low (GPU-scope VRAM overhang only) Β· Bucket: memcorrupt (radeon twin of DF-4524 / DF-4742)

What to run

python3 check_arith.py        # -> arith.log (same content)

No kernel build or hardware needed for the arithmetic; the runtime effect needs an R600-class or RV770 ASIC with UVD + its firmware, which this guest does not have (see env.txt).

Expected output (decisive lines)

exhaustive ds in [1,4095] (one full mod-4096 cycle):
  overrun bytes: min=128 max=128 ; vulnerable residues: 4088/4096 (99.8%)
  vulnerable residues are exactly ds % 4096 in [1,4088]: True
  safe residues (ds % 4096 == 0 or in [4089,4095]): True
control (uvd_v2_2 non-RV770): span<=bo for ALL residues: True

i.e. for every legacy UVD firmware whose datasize % 4096 lands in [1,4088] (99.8% of possible sizes, including all realistic blob sizes), the STACK/SESSION window programmed into the UVD VCPU ends exactly 128 bytes past the end of rdev->uvd.vcpu_bo β€” inside the next VRAM object.

Reproduce on hardware (not possible here)

  1. Bare metal (or GPU-passthrough VM) with an R600/RV610/RV630/RV670/RV620/ RV635/RS780/RS880 or RV770 GPU (radeonkms driver, radeonkmsfw_R600_uvd / RS780 / RV770 firmware loaded).
  2. Trigger UVD use (any VDPAU/video decode via CS on the UVD ring).
  3. Observe the last 128 bytes of the session/stack region (VRAM addresses gpu_addr + 128 + ALIGN(ds) + HEAP + STACK + SESSION*N - 128 .. +0) landing in the VRAM object allocated after vcpu_bo β€” GPU-side read/write of that neighbor object's head.
VERDICT.md
↓ download raw

DF-4832 VERDICT β€” uvd_v1_0.c 128-byte VCPU-cache window overhang

Status: untested (hardware-gated) β€” arithmetic overrun certain, runtime effect fw-behavior-dependent. Impact: GPU-scope VRAM overhang of 128 bytes past a pinned bo. Confidence: speculative (mirrors DF-4524/DF-4742).

The defect

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

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

uvd_v1_0_resume (uvd_v1_0.c:121-135), used for the whole R600 class (r600.c:3080) and for RV770 via uvd_v2_2.c:106, programs the VCPU LMI cache windows shifted +16 window-units (128 bytes) into the bo and then lays out the full fw + HEAP + STACK/SESSION sizes after that offset:

addr = (gpu_addr >> 3) + 16;                          /* +128 B   :121 */
size = RADEON_GPU_PAGE_ALIGN(datasize) >> 3;           /* ALIGN(ds):122  */
addr += size; size = HEAP >> 3;                        /* :126-127 */
addr += size; size = (STACK + SESSION*max_handles)>>3; /* :131-133 */

Total VCPU-visible span = 128 + ALIGN(datasize) + HEAP + STACK + SESSION*N, while the bo ends at ALIGN(datasize+8) + HEAP + STACK + SESSION*N.

  • datasize % 4096 ∈ [1,4088] β†’ ALIGN(datasize+8) == ALIGN(datasize) β†’ span = bo_end + 128. The last 128 bytes of the STACK/SESSION window lie in the VRAM object allocated after vcpu_bo.
  • datasize % 4096 == 0 or ∈ [4089,4095] β†’ the +8 forces one extra GPU page of slack β‰₯ 128 B β†’ fits.
  • Legacy headerless fw is always used for these ASICs (fw_name is only set for BONAIRE+; radeon_uvd.c:126-132,176), and datasize is the raw blob size, so 99.8% of possible sizes are in the vulnerable class (check_arith.py, exhaustive mod-4096 sweep β€” arith.log).

Control: uvd_v2_2 (non-RV770) starts its windows at +0 and sizes region0 ALIGN(datasize+4) ≀ ALIGN(datasize+8) β†’ fits for every residue (proven by the harness). The overhang is specific to the v1.0 +128 layout. Cross-note (out of this file's scope, uvd_v4_2.c:47 uses a +0x200 skip with ALIGN(datasize+4)): the same harness shows a 512-byte-class overhang shape there when fw_header_present β€” filed for the orchestrator's attention, not re-filed here (uvd_v4_2.c was pass-1 clean).

Why it cannot be runtime-verified here

The guest has no AMD ASIC at all β€” QEMU bochs vgapci0 chip=0x11111234, no drm/radeon/agp modules, no hw.drm sysctls (env.txt). The defect additionally requires the r600-era UVD firmware blob and a CS-driven decode session so the VCPU actually touches the window tail (fw-defined behavior). Identical situation to the accepted twins DF-4524 (vce_v3_0, amdgpu) and DF-4742 (vce_v2_0, amdgpu), which were filed on arithmetic proof alone.

Threat model

An unprivileged local user on affected hardware who can submit CS to the UVD ring (video decode) gets the UVD VCPU's session/stack tail mapped over the first 128 bytes of the next VRAM object. Ceiling: GPU-side cross-object info-leak/corruption between GEM bo's (e.g. another user's buffer) β€” not kernel host memory; the window is a GPU-physical (LMI) mapping into VRAM, and past the end of VRAM the LMI simply has no valid target (hang/reset, not host corruption). No kernel memory-safety impact; kernel-controlled inputs only (fixed bo address, fixed constants); nothing in uvd_v1_0.c consumes user-controlled data on these paths.

Negative-check list (what else was hunted in uvd_v1_0.c and killed)

  • uvd_v1_0_init ring accounting: radeon_ring_lock(…, 10) (uvd_v1_0.c:180) vs exactly 10 radeon_ring_writes (:187-203) β€” exact, no ring overflow.
  • uvd_v1_0_fence_emit writes 10 dwords; every caller reserves far more (radeon_ib.c:135 locks 64 + RADEON_NUM_SYNCS*8) β€” no count_dw underflow (and radeon_ring_write, radeon.h:2708-2717, only logs on overflow, staying inside ptr_mask).
  • uvd_v1_0_ring_test: r is always assigned (radeon_ring_lock result at :428) before the timeout branch β€” no uninit read; timeout bounded by usec_timeout.
  • uvd_v1_0_start worst-case ~10.5 s mdelay busy-wait (:329-347) is upstream-shared and reachable only from device init/resume (r600.c:3080, rv770.c:1743) β€” no unpriv trigger path; not filed.
  • uvd_v1_0_ib_execute 32-bit IB base (:486-487) β€” upstream layout, upper bits via UVD_LMI_EXT40_ADDR (:364); IB gpu_addr/length pre-validated by the CS parser (radeon_uvd.c, DF-1655/DF-4500 domain); no kernel-scope effect.
  • WREG32(UVD_FW_START, *((uint32_t*)uvd.cpu_addr)) (:145) reads 4 bytes of a β‰₯ 500 KB mapped bo β€” in bounds.
  • suspend/resume handle-table lifecycle lives in radeon_uvd.c (pass-1/2 findings DF-1655/1656/1657/4500/4501 cover it); uvd_v1_0.c touches no handle tables.

Fix

fix.diff shrinks the fw window by the 128-byte entry skip so the window chain ends exactly at the bo boundary (size = (ALIGN(datasize+8)>>3) - 16), keeping all subsequent windows inside vcpu_bo for every residue. Not kernel-validated (no hardware); arithmetic-exact against the verified layout (check_arith.py proves span == bo_size after the fix for all 4096 residues).

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 radeon GPU on this guest, no baseline to flip. fix.diff is arithmetic-exact: post-fix span == bo_size for every ds in [0,0x2FFFF] (arith.log tail).

fix.diff (applies clean via git apply --check; window chain ends exactly at bo end)
↓ fix.diffper-fix-DF-4832

Confirmed kernel references

Detail

Evidence (decisive lines)

arith.log (exhaustive mod-4096 sweep: overrun min=max=128B, 4088/4096 residues; uvd_v2_2 control clean for all residues; post-fix span==bo_size for all ds in [0,0x2FFFF]); VERDICT.md (full derivation + negative-check list); env.txt (guest GPU absent)

PoC changes

n/a (no seed; arithmetic harness written fresh, DF-4742 style)

Verified recommended fix

Shrink the fw window by the 128B entry skip in uvd_v1_0_resume (size = (RADEON_GPU_PAGE_ALIGN(datasize+8) >> 3) - 16) so the window chain ends at the bo boundary, or clamp windows to bo size v4_0-style

Verdict

uvd_v1_0_resume programs the UVD VCPU LMI cache windows starting 128 bytes into vcpu_bo (addr=(gpu_addr>>3)+16, uvd_v1_0.c:121) but keeps full-size regions (ALIGN(datasize), HEAP, STACK+SESSIONN, uvd_v1_0.c:122-135), while radeon_uvd.c:184-186 sizes the bo at ALIGN(datasize+8)+HEAP+STACK+SESSIONN. For datasize%4096 in [1,4088] (4088/4096 = 99.8% of sizes, incl. every realistic legacy blob; these ASICs always use legacy headerless fw) the STACK/SESSION window ends exactly bo_end+128B, in the VRAM object allocated after vcpu_bo. Affects the whole R600 class (r600.c:3080) and RV770 (uvd_v2_2.c:106). Arithmetic proven exhaustively by check_arith.py (arith.log: min=max=128B overrun; control uvd_v2_2 fits for ALL residues). End-to-end not runnable: guest has no AMD ASIC (bochs chip=0x11111234, no drm modules, env.txt) and the window-tail write is fw-behavior-dependent -- speculative, mirroring accepted twins DF-4524 (vce_v3_0) and DF-4742 (vce_v2_0). GPU-scope only: LMI windows map VRAM, not host memory; ceiling is 128B cross-object GPU read/write between adjacent GEM bo's during video decode (CS-reachable by unpriv users on real hw). No kernel host-memory impact.