radeon_gem_va_ioctl: bo_va use-after-free β set_addr() drops the BO reservation internally, then radeon_gem_va_update_vm() dereferences bo_va unprotected while a racing GEM_CLOSE on the same DRM fd frees it
Summary
The ioctl obtains bo_va under the BO reservation - its only lifetime guarantee - then set_addr() unreserves internally by contract ('Object has to be reserved and gets unreserved by this function!'), after which the ioctl uses bo_va unlocked through the whole update_vm helper (bo/vm derefs, ttm_eu_reserve, mutex_lock on bo_va->vm->mutex, it.start reads). A concurrent GEM_CLOSE on the same fd (or a dup'd fd: same file_priv/vm) blocks on the reservation exactly until set_addr drops it, then rmv->kfree's bo_va (immediate for UNMAP; for MAP deferred to vm->freed which thread A's own clear_freed flushes BEFORE touching bo_va again). Scheduler-latency-wide window, reliably hittable with a two-thread loop; authenticated render-node user on Cayman+ with accel. Panic at minimum; with 128-byte slab grooming, controlled bo/vm pointers drive mutex_lock on arbitrary kernel addresses and vm_bo_update page-table writes per an attacker-shaped vm - a credible path to GPU-mediated arbitrary physical R/W and uid=0. Source-proven interleaving (guest has no radeon GPU - inconclusive runtime, trigger builds+runs to a documented SKIP). Re-lookup-under-re-established-reservation fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4604 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.7 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| radeon_va_uaf.c | β | 7.2 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.log | β | 21 B | view raw | |
| run.log | β | 213 B | view raw | |
| env.txt | β | 313 B | view raw | |
| fix.diff | β | 2.5 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-4604 PoC β radeon_gem_va_ioctl bo_va UAF
What this is
A minimal, self-contained trigger for the bo_va use-after-free in
radeon_gem_va_ioctl() (sys/dev/drm/radeon/radeon_gem.c:702). Two threads on
the same DRM fd: A loops {GEM_CREATE β VA_MAP β VA_UNMAP}, B races
GEM_CLOSE on the same handle, armed so that B is already blocked on the BO
reservation at the moment A's radeon_vm_bo_set_addr() internally drops it.
Build
./build.sh # cc -O2 -Wall -pthread -o radeon_va_uaf radeon_va_uaf.c
No drm headers required (UAPI vendored, BSD 3-arg _IOWR encoding matching
sys/dev/drm/include/uapi/drm/drm.h:796-801).
Run (requires real radeon hardware, Cayman/HD69xx or newer)
./run.sh [/dev/dri/card0] [iterations]
Expected (unpatched kernel): kernel panic / drm fault in
ttm_eu_reserve_buffers, mutex_lock, or radeon_vm_bo_update within
seconds-to-minutes of iterations; with slab grooming, exploitable to uid=0
(see VERDICT.md).
On the audit QEMU guest: there is no radeon GPU; the binary builds and
runs, printing SKIP: no DRM device⦠and exiting rc=3 (see run.log).
This is the expected guest outcome β runtime confirmation needs a physical
Cayman+ machine.
Patched kernel
With fix.diff applied (bo_va re-looked-up under the re-established
reservation), the race window is closed; the trigger must run indefinitely
without corrupting memory.
Files
radeon_va_uaf.cβ trigger sourcebuild.sh/run.shβ exact commandsbuild.log/run.logβ guest output (full, untrimmed)env.txtβ guest environmentVERDICT.mdβ full narrative with path:line prooffix.diffβ validated-by-review unified fixmanifest.json,verdict.json
DF-4604 β radeon_gem_va_ioctl() use-after-free on bo_va (GEM_VA vs GEM_CLOSE race)
- File:
sys/dev/drm/radeon/radeon_gem.c - Class: kernel heap UAF / type confusion (memcorrupt bucket)
- Severity: High β local unprivileged user with DRM access on Cayman-or-newer radeon
- Status: source-proven; runtime verification impossible on the audit guest (no radeon GPU β QEMU cannot emulate AMD GPUs)
TL;DR
radeon_gem_va_ioctl() finds bo_va under the BO reservation, then calls
radeon_vm_bo_set_addr(), which drops the reservation internally, and only
afterwards uses bo_va in radeon_gem_va_update_vm() β with no reservation and
no vm->mutex protecting the first dereferences. A concurrent
DRM_IOCTL_GEM_CLOSE (or close(2) of a dup'd fd) on the same file_priv
runs radeon_gem_object_close() β radeon_vm_bo_rmv() β kfree(bo_va).
Thread A then executes radeon_gem_va_update_vm() on the freed ~128-byte
object: bo_va->bo, bo_va->vm, mutex_lock(&bo_va->vm->mutex), and
radeon_vm_bo_update() on freed (reclaimable) memory.
The exact interleaving
Thread A: DRM_IOCTL_RADEON_GEM_VA (UNMAP) Thread B: DRM_IOCTL_GEM_CLOSE (same fd!)
------------------------------------------------ ------------------------------------------
radeon_gem.c:665 drm_gem_object_lookup() (gobj ref held by A β bo itself safe)
radeon_gem.c:671 radeon_bo_reserve(rbo) β blocks here: rbo reserved by A
radeon_gem.c:677 bo_va = radeon_vm_bo_find() | (lifetime of bo_va is guaranteed ONLY
| | by the BO reservation β see
| | radeon_vm.c:117 "Object has to be
| | reserved!")
radeon_gem.c:696 radeon_vm_bo_set_addr(0,0) |
radeon_vm.c:538 radeon_bo_unreserve() ------+ |
| | +-- wakes, acquires reservation
| *** WINDOW OPEN *** +----β radeon_gem.c:193 reserve OK
radeon_gem.c:702 radeon_gem_va_update_vm(bo_va) radeon_gem.c:199 bo_va = find()
radeon_gem.c:565 tv.bo = &bo_va->bo->tbo radeon_gem.c:201 --ref_count==0
radeon_gem.c:569 radeon_vm_get_bos(bo_va->vm,..) radeon_gem.c:202 radeon_vm_bo_rmv()
radeon_gem.c:573 ttm_eu_reserve_buffers() radeon_vm.c:1127 it.start==0
radeon_gem.c:585 mutex_lock(&bo_va->vm->mutex) radeon_vm.c:1137 kfree(bo_va) βFREE
radeon_gem.c:590 if (bo_va->it.start) |
radeon_gem.c:591 radeon_vm_bo_update(rdev, bo_va,..) | β ALL UAF
For the UNMAP variant the free is immediate (it.start == 0 β straight
kfree). For the MAP variant, radeon_vm_bo_rmv() queues bo_va itself on
vm->freed and thread A's own radeon_gem_va_update_vm() calls
radeon_vm_clear_freed() (radeon_gem.c:586), which does kfree(bo_va)
(radeon_vm.c:1065) and then A dereferences it at radeon_gem.c:590-591. Both
variants UAF.
Why the race is practically hittable
Thread B blocks inside radeon_bo_reserve(rbo, true) while A holds the
reservation during set_addr(). The instant set_addr internally unreserves,
the already-blocked B is woken and runs its entire close path (find β rmv β
kfree) while A is still returning from set_addr β the window is scheduler
latency wide, not instruction width. Looping {GEM_CREATE β VA_MAP β
(UNMAP β₯ GEM_CLOSE)} tens of thousands of times (see radeon_va_uaf.c) is
sufficient on real hardware.
Preconditions
- radeon KMS hardware,
family >= CHIP_CAYMAN,accel_working(gates bothvm_manager.enabledat radeon_gem.c:618 and the close path at radeon_gem.c:188). Cayman (HD 69xx) through the last radeon-supported SI/CIK parts. - An authenticated DRM fd (render node access as an unprivileged user, or any
fd handed out by the display manager) β the ioctls are
DRM_AUTH|DRM_RENDER_ALLOW(radeon_kms.c:912,918). - Both threads on the same fd (or dup'd fds β same
file_priv, samevm).
Exploitability sketch (primary objective: uid=0)
struct radeon_bo_va is ~96-104 bytes β kzalloc 128-size class. After the
free, thread A's radeon_gem_va_update_vm():
1. reads bo_va->bo and bo_va->vm from reclaimed memory,
2. passes &bo_va->bo->tbo into ttm_eu_reserve_buffers() (ww-mutex lock
chains over attacker-influenced pointers),
3. mutex_lock(&bo_va->vm->mutex) on a reclaimed pointer,
4. radeon_vm_bo_update(rdev, bo_va, ...) walks bo_va->vm->page_tables[] and
emits GPU page-table writes per the reclaimed vm.
Grooming the 128-byte slab with a kernel object whose contents the attacker
controls (classic: repeated small kzalloc user-trigerable objects) converts
step 4 into "point GPU PTEs at chosen physical pages", which on this
intentionally-unhardened target (no SMAP/SMEP/KASLR equivalents for the GPU
path) is a direct route to arbitrary physical read/write from the GPU command
processor β uid=0. Even without full grooming, the immediate symptom on real
hardware is a kernel panic in the reservation/mutex machinery.
Verification on the audit guest β honest classification
- Guest: DragonFly 6.5-DEVELOPMENT x86_64, QEMU std/Bochs VGA (
chip=0x11111234). No/dev/dri, no radeon device, no drm in the kernel config. QEMU has no AMD-GPU emulation; the ioctl table for radeon never registers. - The trigger (
radeon_va_uaf.c) builds cleanly on the guest (build.log) and runs, exitingrc=3with the documented SKIP (run.log). - Therefore:
status=inconclusive,reproduced=0β the bug is proven by code trace (every step cited), but cannot be executed on this guest. Runtime confirmation requires a physical Cayman-or-newer machine. Fix validation likewisenot_testablehere.
Recommended fix
Re-look-up bo_va after ttm_eu_reserve_buffers() has re-established the BO
reservation inside radeon_gem_va_update_vm() (and derive bo/vm from
refcounted objects held by the ioctl). See fix.diff.
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 authored against the read-only sys/ tree (never applied); validation impossible on this guest - no radeon hardware, so neither baseline nor patched behavior can be executed. The fix closes the window by construction: every bo_va dereference happens either under the BO reservation re-established by ttm_eu_reserve_buffers() or under vm->mutex, and the bo/vm pointers come from objects whose lifetime the ioctl pins.
fix.diff in this directory; reasoning in VERDICT.md 'Recommended fix' section.
Confirmed kernel references
- sys/dev/drm/radeon/radeon_gem.c:701-702
- sys/dev/drm/radeon/radeon_gem.c:565
- sys/dev/drm/radeon/radeon_gem.c:569
- sys/dev/drm/radeon/radeon_gem.c:585
- sys/dev/drm/radeon/radeon_gem.c:590-591
- sys/dev/drm/radeon/radeon_vm.c:538
- sys/dev/drm/radeon/radeon_gem.c:193-205
- sys/dev/drm/radeon/radeon_vm.c:1137
- sys/dev/drm/drm_gem.c:336-365
Detail
Exploit chain
On real hw: (1) unprivileged user opens render node/authenticated DRM fd; (2) thread A: GEM_CREATE -> VA_MAP, then arms thread B and issues VA_UNMAP; (3) thread B blocks in radeon_bo_reserve inside radeon_gem_object_close, is woken when set_addr internally unreserves, and kfree()s the ~128-byte bo_va; (4) thread A's radeon_gem_va_update_vm() runs on the freed object: reads bo_va->bo and bo_va->vm from reclaimed memory, mutex_lock()s a reclaimed vm, and calls radeon_vm_bo_update() which walks vm->page_tables[] from reclaimed memory; (5) groom the 128-size slab with attacker-controlled kernel data to aim the GPU page-table update at chosen physical pages -> GPU-mediated arbitrary phys r/w -> uid=0. Step (5) not demonstrated (no hardware).
Evidence (decisive lines)
VERDICT.md contains the full path:line interleaving diagram; radeon_va_uaf.c is the self-contained trigger (phase-barrier design so GEM_CLOSE blocks on the reservation exactly when set_addr drops it); run.log shows the guest build+run with the honest SKIP; fix.diff re-looks-up bo_va after ttm_eu_reserve_buffers() re-establishes the reservation.
PoC changes
Seed sketch had Linux 2-arg _IOWR macros and required system drm headers; rewrote with BSD 3-arg _IOWR('d', nr, type) matching sys/dev/drm/include/uapi/drm/drm.h:796-801, vendored the radeon UAPI subset, added render-node fallback and the rc=3 SKIP convention, and redesigned the race harness (phase variable) so the GEM_CLOSE thread is already blocked on the BO reservation at the moment radeon_vm_bo_set_addr() drops it.
Verified recommended fix
Pass refcounted objects (fpriv->vm, rbo from the gobj lookup) into radeon_gem_va_update_vm() and re-lookup bo_va with radeon_vm_bo_find() after ttm_eu_reserve_buffers() has re-established the BO reservation; bail out cleanly if a racing GEM_CLOSE already removed it.
Verdict
Source-proven use-after-free: radeon_gem_va_ioctl (radeon_gem.c:671-702) looks up bo_va under the BO reservation, but radeon_vm_bo_set_addr() drops that reservation internally (radeon_vm.c:538/:581), and the ioctl then passes bo_va to radeon_gem_va_update_vm() whose first dereferences (radeon_gem.c:565/569/573/585/590) are unprotected. A concurrent DRM_IOCTL_GEM_CLOSE (or close of a dup'd fd) on the same file_priv executes radeon_gem_object_close (radeon_gem.c:193-205), acquires the now-free reservation, drops bo_va->ref_count 1->0 and frees bo_va via radeon_vm_bo_rmv()->kfree (radeon_vm.c:1137 immediate for the UNMAP case; radeon_vm.c:1065 via vm->freed for MAP - which thread A's own update_vm then triggers before touching bo_va again at :590). Runtime verification on the audit QEMU guest is impossible: the guest has no radeon GPU (vgapci 0x1111:1234 Bochs, no /dev/dri, no drm in kernel config) and QEMU cannot emulate AMD GPUs. The trigger compiles cleanly on the guest and runs, exiting rc=3 with the documented SKIP. Verification requires a physical Cayman-or-newer radeon machine with accel working.
No comments yet.