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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4164 Β· 6 files
FileTypeDescriptionSize
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) then kfree(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.

VERDICT.md
↓ download raw

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) β€” no gem.mutex, no reference taken on bo; invoked from radeon_pm_set_clocks (:279) with pm.mutex + ring_lock + mclk_lock held β€” 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_init inside radeon_ttm_bo_destroy, immediately followed by kfree(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_work for 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_states name array [5] is only indexed by legacy types 0..4 (enum radeon.h:1206-1225; DPM uses the separate radeon_dpm_print_power_state); legacy combios/atombios parsers never emit the INTERNAL types β†’ no OOB read of the static array.
  • radeon_set_power_state's power_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_vblank only set on successful get).
  • dynpm idle work handler (:1898-1957) is dead code on DFly (never scheduled β€” all schedule_delayed_work call 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4164

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.