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

radeon_pm_fini_dpm never cancels pm.dpm.thermal.work nor clears dpm_enabled while the thermal IRQ outlives pm_fini: post-fini UAF in radeon_dpm_thermal_work_handler

Summary

fini_dpm frees the DPM state via per-asic finis (which kfree ps/ps_priv/priv without NULLing, num_ps stays >0) but never cancels thermal.work and never clears dpm_enabled. The thermal IRQ is still installed at that point (asic fini order: pm_fini before irq teardown) and schedules the work on dpm_enabled. A work item queued across the unload window runs after the frees: both entry checks read the stale-true flag (suspend clears it - that path is safe; fini is the hole), and pick_power_state walks the freed array. cancel_work_sync + flag-clear fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4163 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
fix.diff β€” 1006 B view raw
env.txt β€” 823 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.8 KB view raw

DF-4163 β€” radeon_pm_fini_dpm never cancels pm.dpm.thermal.work nor clears dpm_enabled: post-fini UAF in the thermal work handler

File: sys/dev/drm/radeon/radeon_pm.c (pass 2)

What

radeon_pm_fini_dpm() (radeon_pm.c:1690-1719) tears the DPM state down (radeon_dpm_fini() at :1705 frees rdev->pm.dpm.ps, ps_priv[], rdev->pm.dpm.priv β€” e.g. si_dpm_fini, si_dpm.c:7078-7094, kfree without NULL and num_ps left > 0) but:

  • never calls cancel_work_sync(&rdev->pm.dpm.thermal.work),
  • never sets rdev->pm.dpm_enabled = false.

Meanwhile the thermal IRQ is still installed: the asic fini order runs radeon_pm_fini before the IRQ teardown (si.c si_fini: radeon_pm_fini at :6945, si_irq_fini :6950, radeon_irq_kms_fini after; cik.c: :8731 vs :8736/:8742; same in r600.c/evergreen.c). The IRQ handler schedules the work whenever dpm_enabled is set (if (queue_thermal && rdev->pm.dpm_enabled) schedule_work(...) β€” si.c:6429-6430, r600.c:4343, evergreen.c:4902, cik.c:8125).

Race

thermal event at driver unload (kldunload / device detach):

IRQ thread                            unload thread
-----------                           --------------
IH src 230/231: queue_thermal = true
  dpm_enabled == true (stale)         radeon_pm_fini_dpm()
  schedule_work(thermal.work)           radeon_dpm_disable()
                                         radeon_dpm_fini() -> kfree(ps),
                                           kfree(ps_priv[i]), kfree(priv)
handler runs:
  :881 dpm_enabled? -> still true       (fini never cleared it)
  :895 mutex_lock(pm.mutex)
  compute_clocks -> change_power_state_locked
    :1062 dpm_enabled? -> still true
    :945-947 ps = &rdev->pm.dpm.ps[i]  -> FREED memory (ps dangling,
                                           num_ps > 0)
    -> requested_ps = dangling ps ->
       radeon_dpm_set_power_state -> si_set_power_state derefs
          ps_priv (freed) and rdev->pm.dpm.priv (freed) for SMC
          programming                    <-- UAF read/consume

The mutex does not save the handler: radeon_dpm_fini is called outside pm.mutex (only the preceding radeon_dpm_disable is under it), so nothing orders the handler against the frees.

Preconditions

radeon DPM machine (thermal IRQ armed, i.e. irq.dpm_thermal) whose driver is unloaded/detached while a thermal threshold event fires β€” root-triggered unload racing a hardware/thermal event. Result: UAF read of freed slab memory feeding GPU SMC programming (garbage clocks/voltages) or, with slab-reuse/poisoning, worse.

Fix

cancel_work_sync() + dpm_enabled = false at the top of radeon_pm_fini_dpm, before any state is freed (fix.diff). Closure argument: after the flag is cleared, a late-scheduled straggler handler returns at its entry check (:881) or at the under-mutex recheck in radeon_dpm_change_power_state_locked (:1062) β€” and a handler that already passed the under-mutex check must hold pm.mutex, which forces it to complete before fini's own mutex section, i.e. before anything is freed.

Apply order: DF-4162/fix.diff first, then this diff (generated against the DF-4162-patched tree; both verified to compose).

Status on this audit guest

Hardware-gated (no radeon GPU β€” env.txt); source-certain structure, timing-dependent trigger β†’ per family convention: inconclusive.

VERDICT.md
↓ download raw

DF-4163 β€” VERDICT

Status: inconclusive (race proven at source level, runtime hardware-gated). Expected impact if hardware present: UAF on freed DPM state during driver unload β€” panic most likely; allocator-dependent worse.

Reproduced?

No β€” guest has no radeon GPU (env.txt). The structural facts are all source-verified:

  • radeon_pm_fini_dpm (radeon_pm.c:1690-1719): no cancel_work_sync of pm.dpm.thermal.work anywhere in the tree (grep: the only cancels in radeon_pm.c are cancel_delayed_work_sync(&rdev->pm.dynpm_idle_work) inside #ifdef DUMBBELL_WIP blocks at :458/:1252/:1670); dpm_enabled is never cleared on the fini path (contrast radeon_pm_suspend_dpm :1263 and late-init failure :1625 which do clear it).
  • radeon_dpm_fini β†’ per-asic finis kfree ps/ps_priv[]/priv without NULLing and leave num_ps > 0 (si_dpm.c:7078-7094; rv7xx/btc/cypress equivalents share the pattern).
  • IRQ liveness across the free: asic fini order puts radeon_pm_fini before *_irq_fini/radeon_irq_kms_fini (si.c:6945 vs :6950; cik.c:8731 vs :8736/:8742), and the IH handlers schedule the work on rdev->pm.dpm_enabled (si.c:6429-6430, r600.c:4343, evergreen.c:4902, cik.c:8125) β€” the flag fini leaves set.
  • Handler gating is insufficient only on the fini path: the unlocked entry check (:881) and the under-mutex recheck (:1062, via radeon_dpm_change_power_state_locked) both read the stale-true flag; radeon_dpm_fini runs outside pm.mutex, so the mutex orders nothing against the frees. On suspend the flag IS cleared (:1263) β€” verified that path is safe; fini is the hole.
  • UAF consumption: radeon_dpm_pick_power_state walks rdev->pm.dpm.ps[i].class (:945-947) over freed memory with num_ps > 0; a "matched" ps becomes requested_ps, and radeon_dpm_set_power_state (:1154) hands the freed ps_priv/priv to the asic SMC programming (e.g. si_set_power_state via si_get_pi(rdev)).

Trigger

radeon DPM machine with internal thermal sensor armed; unload/detach the driver while a thermal threshold event lands in the window (IRQ β†’ schedule_work β†’ pm_fini frees β†’ handler runs). Unload is root-gated (kldunload radeon / device detach), so the practical severity ceiling is a root-initiated memory corruption / panic β€” filed Low per the rubric ("requires already-privileged user"), not Medium.

Fix validation

fix.diff: cancel_work_sync(&rdev->pm.dpm.thermal.work); rdev->pm.dpm_enabled = false; at the head of radeon_pm_fini_dpm β€” before the mutex section and before radeon_dpm_fini. Closure: post-clear stragglers exit at :881 or :1062; pre-clear mutex holders complete before fini's mutex section; the cancel flushes anything queued before the clear. Generated against the DF-4162-patched tree β€” apply DF-4162/fix.diff first (compose order verified). No runtime validation possible on this guest.

Adjacent killed candidates

  • radeon_hwmon_fini sensor kfree vs in-flight radeon_hwmon_refresh β€” KILLED: the sensor task thread calls st->func(st->arg) while holding the task-thread lock (kern_sensors.c:301-344), and sensor_task_unregister takes that lock (:238-250), so it cannot return while a refresh is running; after running=0 no further invocation occurs. No UAF.
  • thermal handler vs suspend: radeon_pm_suspend_dpm clears dpm_enabled under the mutex before disabling β€” the :881/:1062 checks close that window. Only fini is defective.
  • radeon_pm_acpi_event_handler / radeon_pm_late_init failure paths set dpm_enabled=false β€” verified consistent.

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 closes the fini window (flush + flag-clear before frees; stragglers exit at the :881/:1062 checks); cannot be runtime-validated without radeon hardware.

findings/poc/DF-4163/fix.diff
↓ fix.diffper-fix-DF-4163

Confirmed kernel references

Detail

Exploit chain

On radeon hardware: root-initiated driver unload (kldunload/detach) racing a thermal threshold IRQ -> UAF read of freed kzalloc memory driving GPU SMC clock/voltage programming; realistic outcome panic/garbage-DPM, escalation improbable (root already required to unload).

Evidence (decisive lines)

VERDICT.md (liveness/ordering proof + killed candidates); env.txt (hardware absence + md5 match); fix.diff (cancel_work_sync + dpm_enabled=false at fini head; composed against DF-4162-patched tree, apply DF-4162 first)

PoC changes

No seed PoC; trigger is a hardware unload+thermal race documented in README.md.

Verified recommended fix

cancel_work_sync(&rdev->pm.dpm.thermal.work) + rdev->pm.dpm_enabled = false at the top of radeon_pm_fini_dpm before any DPM state is freed (see fix.diff).

Verdict

Structural UAF proven at source level, trigger timing-dependent and hardware-gated (inconclusive per family convention). radeon_pm_fini_dpm (radeon_pm.c:1690-1719) frees the DPM state via radeon_dpm_fini (:1705; per-asic finis kfree ps/ps_priv[]/priv without NULLing and leave num_ps>0, e.g. si_dpm.c:7078-7094) but never calls cancel_work_sync(&rdev->pm.dpm.thermal.work) and never clears rdev->pm.dpm_enabled. The thermal IRQ outlives pm_fini in every asic fini order (si.c:6945 pm_fini before :6950 irq teardown; cik.c:8731 vs :8736/:8742) and its IH handler schedules the work whenever dpm_enabled is set (si.c:6429-6430, r600.c:4343, evergreen.c:4902, cik.c:8125). A work item queued across the unload window therefore runs after the frees: the handler's entry check (:881) and the under-mutex recheck in radeon_dpm_change_power_state_locked (:1062) both read the stale-true flag (suspend DOES clear it at :1263 -- verified, suspend path is safe; fini is the hole), and radeon_dpm_fini runs outside pm.mutex so nothing orders the handler against the frees. Consumption: radeon_dpm_pick_power_state walks rdev->pm.dpm.ps[i].class over freed memory (:945-947), the dangling ps becomes requested_ps, and radeon_dpm_set_power_state (:1154) feeds freed ps_priv/priv into the asic SMC programming. Guest: virtio VGA only, zero 0x1002 devices (env.txt) -> not runtime-testable. Killed adjacent candidates: radeon_hwmon_fini sensor kfree is SAFE (sensor task thread holds its lock across st->func, kern_sensors.c:301-344, and sensor_task_unregister takes that lock, :238-250 -- no in-flight-after-unregister window); suspend-side thermal race closed by :1263.