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

NULL rdev->uvd_fw dereference in uvd_v4_2_resume via the unguarded KV DPM ungate path (kv_dpm.c:1628) β€” kernel panic on suspend/pm-fini of a DPM-enabled CIK APU without UVD firmware

Summary

uvd_v4_2.c:51 reads rdev->uvd_fw->datasize with no guard; the cik_uvd_start caller is guarded (has_uvd/vcpu_bo) but kv_dpm_powergate_uvd(rdev,false) calls it directly with none. On KV/CIK APUs with SMC fw but no UVD fw kmods (natural on DFly - all radeon fw is separate opt-in kmods), every suspend (radeon_pm_suspend runs BEFORE the has_uvd guard) or radeonkms unload panics. REPRODUCED on guest: production uvd_v4_2_resume from freshly built radeon.ko -> Fatal trap 12 fault va=0x10 at uvd_v4_2_resume+0x30, twice (panic.txt, panic.2.txt); one-line guard applied in guest /usr/src + rebuilt radeon.ko -> identical trigger survives returning -EINVAL (run.fixed.log); guest recovered clean. Physical-user lid-close or root unload; DoS ceiling only.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5230 Β· 18 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 6.5 KB ↓ raw
df5230.c β€” 3.4 KB view raw
Makefile β€” 444 B ↓ download
build.sh β€” 125 B view raw
run.sh β€” 139 B view raw
panic.txt β€” 1.9 KB view raw
panic.2.txt β€” 1.0 KB view raw
run.baseline.log β€” 0 B ↓ download
run.baseline2.log β€” 7 B view raw
run.fixed.log β€” 405 B view raw
fix_run.log β€” 405 B view raw
fix_build.log β€” 955 B view raw
load_modules.log β€” 354 B view raw
env.txt β€” 165 B view raw
fix.diff β€” 695 B view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.7 KB view raw

DF-5230 β€” NULL rdev->uvd_fw dereference in uvd_v4_2_resume() via the KV DPM ungate path β†’ kernel panic

File: sys/dev/drm/radeon/uvd_v4_2.c:51 (trigger chain via sys/dev/drm/radeon/kv_dpm.c:1628) Severity: Low (hardware+firmware-config gated local DoS) Β· Bucket: base:dev Status: reproduced (panic) on the QEMU guest with a synthetic-state harness calling the production function; the production trigger chain is proven statically link-by-link below.

What to run

./build.sh     # on the guest: build drm.ko, radeon.ko (baseline), df5230.ko
./run.sh       # kldload drm.ko radeon.ko df5230.ko  -> instant panic

Expected (serial console / vm.sh log):

DF5230: pre   uvd_fw=0 fw_header_present=0 gpu_addr=0x0 max_handles=10
DF5230: calling production uvd_v4_2_resume() (radeon.ko) -- expect panic at uvd_v4_2.c:51
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x10
Stopped at  uvd_v4_2_resume+0x30:  movq 0x10(%rdx),%rdx

0x10 is offsetof(struct firmware, datasize) (sys/sys/firmware.h:51-56: name 8B + data 8B + datasize) β€” the read at uvd_v4_2.c:51 (rdev->uvd_fw->datasize) with uvd_fw == NULL. The fault happens before the first WREG32 (uvd_v4_2.c:52), so no MMIO is touched.

Fix validation (same trigger, one-line guard)

  1. vm.sh reset with-src (clean source)
  2. patch -p1 < fix.diff in the guest's /usr/src, rebuild radeon.ko
  3. identical kldload df5230.ko β†’
DF5230: pre   uvd_fw=0 fw_header_present=0 gpu_addr=0x0 max_handles=10
DF5230: calling production uvd_v4_2_resume() (radeon.ko) -- expect panic at uvd_v4_2.c:51
DF5230: survived, uvd_v4_2_resume returned -22

Guest stays up; kldload succeeds. Baseline panicked twice (once on the original spawn, once from a fresh vm.sh reset with source reverted), identical signature both times (run.baseline.log, panic.txt, run.baseline2.log, panic.2.txt).

Files

  • df5230.c, Makefile β€” the harness kld (zeroed struct radeon_device = the exact post-failed-radeon_uvd_init() state; calls the production uvd_v4_2_resume compiled into radeon.ko)
  • build.sh, run.sh β€” exact commands
  • panic.txt, panic.2.txt β€” serial-console panic transcripts (2 runs)
  • run.baseline.log, run.baseline2.log, run.fixed.log, load_modules.log
  • fix.diff β€” one-line guard (git apply --check clean)
  • env.txt β€” guest environment
VERDICT.md
↓ download raw

DF-5230 VERDICT β€” NULL uvd_fw deref in uvd_v4_2_resume() (kv DPM ungate path)

Status: reproduced (kernel panic), synthetic-state harness calling the production function; production trigger chain proven statically link-by-link. Impact: panic (local DoS on CIK/KV APU hardware with a mixed fw config). Confidence: certain (the deref itself is mechanically demonstrated twice; the field trigger's hw/fw precondition is stated precisely below).

The defect

uvd_v4_2_resume() reads rdev->uvd_fw->datasize at sys/dev/drm/radeon/uvd_v4_2.c:51 with no uvd_fw/vcpu_bo/has_uvd guard.

Two callers exist. One is guarded; the other is not:

  • cik_uvd_start() (cik.c:8167-8175) returns early when !rdev->has_uvd, and the radeon_uvd_resume() it calls first checks vcpu_bo == NULL (radeon_uvd.c:286). Safe.
  • kv_dpm_powergate_uvd(rdev, false) β€” the UVD un-gate branch β€” calls uvd_v4_2_resume(rdev) directly at kv_dpm.c:1628 with no guard at all. (The ungate even calls uvd_v1_0_start() right after, kv_dpm.c:1629.)
system suspend / pm_fini / dpm-disable
  -> cik_suspend()                     cik.c:8545 (radeon_pm_suspend FIRST β€”
                                        the `if (rdev->has_uvd)` guard at
                                        cik.c:8550 sits BELOW this call)
  -> radeon_pm_suspend()               radeon_pm.c:1269-1271
  -> radeon_pm_suspend_dpm()           radeon_pm.c:1257
  -> radeon_dpm_disable(rdev)          radeon.h:2797 macro
  -> kv_dpm_disable()                  kv_dpm.c:1305,1316 (asic bind
                                        radeon_asic.c:2285)
  -> kv_dpm_powergate_uvd(rdev,false)  kv_dpm.c:1608
       [caps_uvd_pg == true unconditionally, kv_dpm.c:2773;
        pi->uvd_power_gated was flipped to true at boot by
        kv_dpm_late_enable() -> powergate_uvd(true), kv_dpm.c:1300 β€”
        so the `== gate` early-return at kv_dpm.c:1612 does NOT fire]
  -> uvd_v4_2_resume(rdev)             kv_dpm.c:1628   NO GUARD
  -> rdev->uvd_fw->datasize            uvd_v4_2.c:51   uvd_fw == NULL

radeon_uvd_init() sets fw_header_present=false (radeon_uvd.c:137) and max_handles=10 (:138) before attempting the firmware load, and on CIK the load of radeonkmsfw_bonaire_uvd (+:176 fallback radeonkmsfw_BONAIRE_uvd) fails when the fw kmods are not loaded β†’ uvd_fw stays NULL β†’ cik.c:8150-8157 clears has_uvd β†’ UVD never starts, but DPM is independent: on KAVERI/KABINI/MULLINS (KV family, kv_dpm_*) DPM comes up from the SMC firmware (kv_smc.c), kv_dpm_late_enable() power-gates UVD at boot, and the first suspend / pm-fini then un-gates it through the unguarded path above.

Preconditions (why Low)

  1. KV-family CIK APU (KAVERI/KABINI/MULLINS β€” radeonkms, DPM capable).
  2. DPM enabled β‡’ SMC fw kmod loaded (radeonkmsfw_*_smc).
  3. UVD fw kmods (radeonkmsfw_bonaire_uvd / radeonkmsfw_BONAIRE_uvd) NOT loaded β€” radeon_uvd_init() failed, uvd_fw == NULL.

On DragonFly all radeon firmware ships as separate opt-in kmods (sys/dev/drm/radeonfw/*/), so "SMC loaded, UVD not" is a natural misconfiguration, not a contrived one. Trigger: any system suspend (lid close / S3 β€” an unprivileged physical action), kldunload radeonkms (radeon_pm_fini_dpm, radeon_pm.c:1694), or a power_method sysfs switch. Result: deterministic NULL-deref panic. Ceiling = DoS; the fault is a pure data read (datasize, offset 0x10) β€” no write primitive, no info leak.

Verification on this guest (no AMD GPU β€” env.txt: NO_AMD_PCI)

The full suspend chain needs KV hardware, so the harness (df5230.c) reconstructs the exact post-failed-init device state (zeroed struct radeon_device + max_handles=10, i.e. fw_header_present=false, gpu_addr=0, uvd_fw=NULL β€” the state kv_dpm.c:1628 would pass) and calls the production uvd_v4_2_resume() from the freshly built radeon.ko (the module that ships the function; no code copied or stubbed):

  • baseline Γ—2 (second from a fresh vm.sh reset with source reverted): Fatal trap 12 ... fault virtual address = 0x10 ... Stopped at uvd_v4_2_resume+0x30: movq 0x10(%rdx),%rdx β€” guest down (panic.txt, panic.2.txt).
  • patched (fix.diff applied in guest /usr/src, radeon.ko rebuilt, identical trigger): DF5230: survived, uvd_v4_2_resume returned -22, guest up (run.fixed.log).

The faulting instruction movq 0x10(%rdx),%rdx is the datasize load at uvd_v4_2.c:51 (0x10 = offsetof(struct firmware, datasize), sys/sys/firmware.h:51-56), and it fires before the first WREG32 (uvd_v4_2.c:52), so the synthetic run exercises precisely the audited deref and nothing else.

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

  • Window-chain overhang past vcpu_bo (+512 B with the shipping fw) β€” real, filed separately as DF-5229 (family of DF-4832/DF-4524/DF-4742).
  • uvd_fw NULL on the guarded caller (cik.c:8167): killed β€” has_uvd covers it; radeon_uvd_resume() also checks vcpu_bo (radeon_uvd.c:286).
  • Boot-time gate path (kv_dpm_late_enable β†’ uvd_v1_0_stop): killed β€” register writes only (uvd_v1_0.c:391-411), no fw deref.
  • Integer width: addr u64, WREG32 truncations are by-design (bits 28-31 at :67, bits 32-39 at :71); size u32 values all small constants β€” killed.
  • max_handles sourced from fw version (radeon_uvd.c:165-166, ≀30) β€” handles[RADEON_MAX_UVD_HANDLES] is 30 (radeon.h:1674,1685) β€” no overflow β€” killed (and DF-5229 shows SESSION*N is bounded by bo sizing anyway).
  • This file consumes no user-controlled data at all (registers + fw-derived scalars only) β€” the only user-reachable surface is via CS on real hw, which is DF-1655/DF-4500/DF-4501 territory in radeon_uvd.c, not here.

fix.diff (one-line guard + comment, git apply --check clean): return -EINVAL when rdev->uvd_fw == NULL in uvd_v4_2_resume, mirroring the has_uvd/vcpu_bo guards of the other two paths. Validated on-guest above. (An alternative placement β€” guard in kv_dpm_powergate_uvd() β€” fixes only the known caller; the function-local guard also covers future callers.)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline (stock radeon.ko): identical trigger panics (fault VA 0x10 at uvd_v4_2_resume+0x30), twice, second from a fresh vm.sh reset with reverted source. Patched (fix.diff in guest /usr/src, radeon.ko rebuilt): identical trigger returns -22, prints 'survived', guest stays up, kldload succeeds. The previously-observed panic is GONE under the fix.

fix_run.log (patched-kernel run: survived/returned -22, guest up); fix_build.log (patch + incremental rebuild transcript); panic.txt/panic.2.txt (baseline); fix.diff
↓ fix.diffper-fix-DF-5230

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt + panic.2.txt (serial console: DF5230 markers, Fatal trap 12, fault VA 0x10, Stopped at uvd_v4_2_resume+0x30 movq 0x10(%rdx),%rdx -- twice, second from fresh reset); run.fixed.log / fix_run.log (patched radeon.ko: 'survived, uvd_v4_2_resume returned -22', guest up); VERDICT.md (full link-by-link production trigger chain with path:line); fix.diff (git apply --check clean); env.txt (no AMD PCI device on guest)

PoC changes

no seed; harness written fresh (df5230.c: zeroed struct radeon_device + max_handles=10 == post-failed-radeon_uvd_init state, calls production radeon.ko symbol; MODULE_DEPEND removed because radeon.ko declares no MODULE_VERSION so a versioned dep always fails)

Verified recommended fix

one-line guard in uvd_v4_2_resume: if (rdev->uvd_fw == NULL) return -EINVAL; (mirrors has_uvd/vcpu_bo guards of the other paths)

Verdict

uvd_v4_2_resume() dereferences rdev->uvd_fw->datasize at sys/dev/drm/radeon/uvd_v4_2.c:51 with no guard. The cik_uvd_start() caller is guarded (has_uvd, cik.c:8167; vcpu_bo, radeon_uvd.c:286), but the KV DPM ungate caller is not: any suspend or pm-fini on a DPM-enabled KV/CIK APU (KAVERI/KABINI/MULLINS) whose UVD firmware kmods are absent (uvd_fw NULL after failed radeon_uvd_init, radeon_uvd.c:137/176; has_uvd cleared cik.c:8156 while DPM stays up from SMC fw) reaches kv_dpm.c:1628 via cik.c:8545 -> radeon_pm.c:1257 -> radeon.h:2797 -> kv_dpm.c:1305,1316,1608 (caps_uvd_pg always true kv_dpm.c:2773; uvd_power_gated set true at boot kv_dpm.c:1300) and panics. The guest has no AMD GPU, so verification used a harness kld that reconstructs the exact post-failed-init radeon_device state (uvd_fw=NULL, fw_header_present=false, gpu_addr=0, max_handles=10) and calls the PRODUCTION uvd_v4_2_resume compiled into a freshly built radeon.ko: deterministic panic 'fault virtual address = 0x10' (offsetof(struct firmware, datasize)) at 'uvd_v4_2_resume+0x30: movq 0x10(%rdx),%rdx', reproduced twice, the second from a fresh vm.sh reset with reverted source; fault precedes the first WREG32 so no MMIO is touched. With fix.diff (one-line uvd_fw==NULL -> -EINVAL guard) applied in the guest /usr/src and radeon.ko rebuilt, the identical trigger prints 'survived, uvd_v4_2_resume returned -22' and the guest stays up. Field trigger is hardware-gated (KV APU + SMC-without-UVD fw config); impact ceiling is a local DoS panic (pure data read, no write primitive).