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)
PoC verification
Evidence pack
findings/poc/DF-4163 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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): nocancel_work_syncofpm.dpm.thermal.workanywhere in the tree (grep: the only cancels in radeon_pm.c arecancel_delayed_work_sync(&rdev->pm.dynpm_idle_work)inside#ifdef DUMBBELL_WIPblocks at :458/:1252/:1670);dpm_enabledis never cleared on the fini path (contrastradeon_pm_suspend_dpm:1263 and late-init failure :1625 which do clear it).radeon_dpm_finiβ per-asic finis kfreeps/ps_priv[]/privwithout NULLing and leavenum_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_finibefore*_irq_fini/radeon_irq_kms_fini(si.c:6945 vs :6950; cik.c:8731 vs :8736/:8742), and the IH handlers schedule the work onrdev->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_finiruns outsidepm.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_statewalksrdev->pm.dpm.ps[i].class(:945-947) over freed memory withnum_ps> 0; a "matched" ps becomesrequested_ps, andradeon_dpm_set_power_state(:1154) hands the freedps_priv/privto the asic SMC programming (e.g. si_set_power_state viasi_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_finisensor kfree vs in-flightradeon_hwmon_refreshβ KILLED: the sensor task thread callsst->func(st->arg)while holding the task-thread lock (kern_sensors.c:301-344), andsensor_task_unregistertakes that lock (:238-250), so it cannot return while a refresh is running; afterrunning=0no further invocation occurs. No UAF.- thermal handler vs suspend:
radeon_pm_suspend_dpmclearsdpm_enabledunder the mutex before disabling β the :881/:1062 checks close that window. Only fini is defective. radeon_pm_acpi_event_handler/radeon_pm_late_initfailure paths setdpm_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_testablefix.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
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.
No comments yet.