radeon_unmap_vram_bos walks rdev->gem.objects without gem.mutex while unprivileged GEM create/close list_add/kfree the same list: UAF read / list corruption race in the PM clock-change path
Summary
The helper, called on every legacy PM clock change, walks the global GEM object list with no lock and no per-object reference, while all mutations are under gem.mutex by writers reachable from unprivileged drm ioctls/handle-close. list_for_each_entry_safe only guards removal of the current element; a destroyed 'n' target or racing tail insertion turns the next iteration's mem_type read into a UAF read of freed memory. PM-side exposure: profile-method machines (pre-DPM asics or firmware-missing fallback) via ACPI events, fbcon init/resume, fini_old. Hold-the-mutex fix (all holders guard pure list ops - no inversion).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4164 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| fix.diff | β | 934 B | view raw | |
| env.txt | β | 834 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-4164 β radeon_unmap_vram_bos() walks rdev->gem.objects without gem.mutex while unprivileged GEM create/close mutates and frees it
File: sys/dev/drm/radeon/radeon_pm.c (pass 2)
What
radeon_unmap_vram_bos() (radeon_pm.c:146-157), called from
radeon_pm_set_clocks() (:279) on every legacy PM clock change, walks the
global GEM object list:
list_for_each_entry_safe(bo, n, &rdev->gem.objects, list) {
if (bo->tbo.mem.mem_type == TTM_PL_VRAM)
ttm_bo_unmap_virtual(&bo->tbo);
}
with no lock and no per-object reference. Every mutation of that list is
done under rdev->gem.mutex by its writers:
- GEM create:
radeon_gem_object_createβlist_add_tail(&robj->list, ...)(radeon_gem.c:89-91), - GEM free:
radeon_ttm_bo_destroyβlist_del_init(&bo->list)thenkfree(bo)(radeon_object.c:68-85), - force-delete (radeon_object.c:441-443).
Both sides are reachable from unprivileged contexts: any local user with
access to the radeon drm node allocates GEM VRAM objects (ioctls) and frees
them by closing/unmapping handles β radeon_ttm_bo_destroy runs from TTM when
the last reference drops, concurrent with the PM path.
The _safe iterator is not sufficient: it only guards removal of the element
currently pointed at (and even that assumes serialized writes); racing
insertions and the kfree of n's target turn the walk into a UAF read of
bo->tbo.mem.mem_type / bo->tbo and potential list corruption.
Reachability of the PM side on DFly
radeon_pm_set_clocks is called from radeon_pm_compute_clocks_old
(:1755/:1767/:1778/:1801 β pm_method PROFILE/DYNPM), the ACPI event handler
(:88 β AC plug/unplug under PM_PROFILE_AUTO), radeon_pm_fini_old (:1660),
and radeon_pm_compute_clocks from radeon_device.c:1816/:1929 (fbcon init /
resume). DPM-method machines do not reach it
(radeon_dpm_change_power_state_locked never calls the unmap helper), so the
exposed population is: pre-DPM asics (r100βrv570/rs4xx) and any card that fell
back to PM_METHOD_PROFILE because rlc/smc firmware is missing
(radeon_pm.c:1533-1542/1571-1582). PM_METHOD_DYNPM is unreachable on DFly
(only settable via the #ifdef DUMBBELL_WIP sysfs attrs, :354-786).
Concrete unprivileged scenario: console user runs an X session (GEM churn: create/close loops) on a profile-mode radeon while AC attach/detach events (or a resume) drive clock switches β race.
Fix
Hold rdev->gem.mutex across the walk (fix.diff). All existing gem.mutex
holders only guard pure list operations (never nested with ttm/reservation
locks), so wrapping ttm_bo_unmap_virtual() introduces no lock-order
inversion.
Status on this audit guest
Hardware-gated (no radeon GPU β env.txt). Source-structure certain, timing race β inconclusive per family convention.
DF-4164 β VERDICT
Status: inconclusive (unlocked-walk structure certain, race timing hardware-gated). Expected impact if hardware present: UAF read / list corruption from an unprivileged-triggerable race β panic (DoS); controlled exploitation unlikely.
Reproduced?
No β guest has no radeon GPU (env.txt). Structure proven from source:
- Walker:
radeon_unmap_vram_bos(radeon_pm.c:146-157) β nogem.mutex, no reference taken onbo; invoked fromradeon_pm_set_clocks(:279) withpm.mutex+ring_lock+mclk_lockheld β none of which serialize GEM-object creation/destruction. - Writers, all under
gem.mutex: - create: radeon_gem.c:89-91 (
list_add_tail), - destroy: radeon_object.c:77-79 (
list_del_initinsideradeon_ttm_bo_destroy, immediately followed bykfree(bo)at :84), - force delete: radeon_object.c:441-443.
- Unprivileged reachability of both sides: GEM create/close via drm ioctls and
fd close; PM-side triggers: ACPI power-profile events (:88), resume/fbcon
(radeon_device.c:1816/:1929), fini_old. DPM-method machines are not exposed
(change_power_state_locked does not call the unmap helper) β exposure is
profile-method hw: pre-DPM asics or firmware-missing fallbacks
(radeon_pm.c:1533-1542/1571-1582). PM_METHOD_DYNPM is dead on DFly (its only
entry is the WIP'd sysfs block :354-786;
schedule_delayed_workfor the dynpm idle work is likewise fully WIP'd β the work can never start).
Why _safe doesn't save it
list_for_each_entry_safe prefetches n = bo->list.next. If n's object is
destroyed between the prefetch and the loop's next iteration, the iteration
dereferences freed memory (bo = n; bo->tbo.mem.mem_type). Concurrent
list_add_tail at the tail can equally strand/rotate the walk. Only a lock
(or per-object refs shared with the destroy path) closes it.
Fix validation
fix.diff wraps the walk in rdev->gem.mutex. Lock-order analysis: every
gem.mutex critical section in the tree touches only list pointers
(verified: radeon_gem.c:89-91, 794-795; radeon_object.c:77-79, 441-443 β
none nest ttm/reservation locks), so holding it across
ttm_bo_unmap_virtual() cannot create an inversion with existing users.
git apply --check clean. No runtime validation possible on this guest.
Adjacent negative results (this hunt)
radeon_pm_print_statesname array [5] is only indexed by legacy types 0..4 (enum radeon.h:1206-1225; DPM uses the separateradeon_dpm_print_power_state); legacy combios/atombios parsers never emit the INTERNAL types β no OOB read of the static array.radeon_set_power_state'spower_state[requested_*_index]indices come from the fixed profile tables / dynpm enumeration β bounded.- vblank get/put pairing in
radeon_pm_set_clocks(:281-307) is symmetric (req_vblankonly set on successful get). - dynpm idle work handler (:1898-1957) is dead code on DFly (never scheduled
β all
schedule_delayed_workcall sites are#ifdef DUMBBELL_WIP).
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 serializes the walk with the same mutex all list mutators already use; no lock-order inversion (existing holders guard pure list ops only); cannot be runtime-validated without radeon hardware.
findings/poc/DF-4164/fix.diff
Confirmed kernel references
Detail
Exploit chain
On profile-method radeon hardware: unprivileged console user churns GEM VRAM objects (create/close loop via drm ioctls) while AC plug events or a resume drive radeon_pm_set_clocks -> radeon_unmap_vram_bos iterates into a concurrently freed radeon_bo -> UAF read/list corruption -> panic. No write primitive.
Evidence (decisive lines)
VERDICT.md (lock analysis + why _safe is insufficient + adjacent kills); env.txt (hardware absence + md5 match); fix.diff (gem.mutex around the walk, lock-order analysis: all existing holders guard pure list ops only, no inversion; git apply --check clean)
PoC changes
No seed PoC; race trigger documented in README.md (GEM churn + AC event).
Verified recommended fix
Hold rdev->gem.mutex across the gem.objects walk in radeon_unmap_vram_bos (see fix.diff).
Verdict
Unlocked-walk structure source-certain, race timing hardware-gated (inconclusive per family convention). radeon_unmap_vram_bos (radeon_pm.c:146-157) walks rdev->gem.objects with no gem.mutex and no per-bo reference, invoked from radeon_pm_set_clocks (:279) under pm.mutex/ring_lock/mclk_lock -- none of which serialize GEM object lifetime. Writers mutate the same list under gem.mutex: GEM create list_add_tail (radeon_gem.c:89-91) and radeon_ttm_bo_destroy's list_del_init+kfree(bo) (radeon_object.c:68-85), both reachable from unprivileged drm ioctls/handle-close. list_for_each_entry_safe only guards removal of the current element; a destroyed 'n' target or racing tail insertion turns the next iteration's bo->tbo.mem.mem_type read into a UAF read of freed memory (and can corrupt the walk). PM-side exposure: profile-method machines (pre-DPM asics or the rlc/smc firmware-missing fallback, radeon_pm.c:1533-1542/1571-1582) via ACPI events (:88), fbcon init/resume (radeon_device.c:1816/:1929) and fini_old; DPM-method machines never call the helper. PM_METHOD_DYNPM is dead on DFly (only entry is the #ifdef DUMBBELL_WIP sysfs block :354-786). Guest: virtio VGA only, zero 0x1002 devices (env.txt) -> not runtime-testable. Impact ceiling: unprivileged local panic (DoS); escalation from a list-walk UAF read is unlikely.
No comments yet.