radeon_bo_get_surface_reg steals surface registers from other BOs under no lock: NULL-deref panic and 4-byte UAF write into freed radeon_bo slab
Summary
The device-global surface_regs[8] registry is walked and stolen while holding only the requesting BO's reservation; the victim BO behind reg->bo is dereferenced (pin_count, unmap_virtual) and written (surface_reg=-1) with no synchronization against the victim's final-unref teardown (clear_surface_reg then kfree). A concurrent GEM close of the victim turns the reads into a NULL-deref panic, or a kfree landing between read and use yields a UAF: a fixed-value 4-byte write of 0xFFFFFFFF at offsetof(radeon_bo, surface_reg) into a freed slab-groomable object. Unpriv reach: SET_TILING(SURFACE) -> mmap fault -> get_surface_reg; occupying all 8 regs forces the steal. PoC built clean on guest (no radeon GPU - inconclusive). Device-level surface_regs_lock fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4584 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df4584_trigger.c | β | 6.8 KB | view raw | |
| build.sh | β | 111 B | view raw | |
| run.sh | β | 137 B | view raw | |
| build.log | β | 9 B | view raw | |
| run.log | β | 682 B | view raw | |
| env.txt | β | 757 B | view raw | |
| VERDICT.md | β | 5.8 KB | β raw | |
| README.md | β | 1.5 KB | β raw | |
| fix.diff | β | 2.8 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4584 evidence pack β radeon surface-reg steal race
- Finding:
radeon_bo_get_surface_reg()steals surface registers from other BOs without any lock protecting the victim β NULL-deref panic / slab UAF write (old_object->surface_reg = -1on freedradeon_bo). - File:
sys/dev/drm/radeon/radeon_object.c:601-669(steal scan + clear). - Trigger:
df4584_trigger.c(self-contained, pthreads; render node or card node).
Build
cc -O2 -Wall -pthread -o df4584_trigger df4584_trigger.c # β build.log
Run
./df4584_trigger /dev/dri/renderD128 # β run.log
./df4584_trigger /dev/dri/card0
Expected
- On vulnerable radeon hardware: kernel panic (NULL deref at
radeon_object.c:628/:641) within minutes of racing, or slab corruption
reports (
zone corruption/ debug output) from the UAF write at :642. - On this audit guest: exit 2 with
open /dev/dri/renderD128: No such file or directoryβ no radeon GPU (virtio VGA only), no radeonkms.ko. Seeenv.txt,run.log.
Contents
df4584_trigger.cβ race trigger (two pthreads: register vs destroy)build.sh/run.shβ exact commandsbuild.log/run.logβ full guest output (untrimmed)env.txtβ guest environment proofVERDICT.mdβ full narrative + interleaving analysisfix.diffβ device-lock fix (git-apply-able; not applied to sys/)manifest.json/verdict.jsonβ machine-readable results
DF-4584 β radeon_bo_get_surface_reg cross-BO steal race (NULL-deref / slab UAF write)
One-line
radeon_bo_get_surface_reg() steals hardware surface registers from other
BOs with no lock covering the victim: reg->bo (a pointer to another
radeon_bo) is read, dereferenced (old_object->pin_count, later
ttm_bo_unmap_virtual(&old_object->tbo)) and written
(old_object->surface_reg = -1) while the victim's final unref path
(radeon_ttm_bo_destroy β radeon_bo_clear_surface_reg β kfree(bo)) can
run concurrently, giving a NULL-deref panic (easy) or a 4-byte 0xFFFFFFFF
write at a fixed offset into freed kmalloc-slab memory (harder).
Full narrative
Root cause (all in sys/dev/drm/radeon/radeon_object.c)
radeon_bo_get_surface_reg()opens withlockdep_assert_held(&bo->tbo.resv->lock.base)(radeon_object.c:609) β it holds the reservation of the BO being registered only.- The registry
rdev->surface_regs[RADEON_GEM_MAX_SURFACES=8](radeon.h:2406, radeon.h:449) is device-global. When all 8 slots are busy, the function picks a victim ("steal"):
c
620 steal = -1;
621 for (i = 0; i < RADEON_GEM_MAX_SURFACES; i++) {
623 reg = &rdev->surface_regs[i];
624 if (!reg->bo) break;
627 old_object = reg->bo;
628 if (old_object->pin_count == 0) /* <-- victim deref, unlocked */
629 steal = i;
630 }
...
637 reg = &rdev->surface_regs[steal];
638 old_object = reg->bo; /* <-- re-read, unlocked */
641 ttm_bo_unmap_virtual(&old_object->tbo); /* <-- deref */
642 old_object->surface_reg = -1; /* <-- WRITE to victim */
- The victim teardown path is
radeon_ttm_bo_destroy(radeon_object.c:69-86) βradeon_bo_clear_surface_reg(radeon_object.c:656-669) βkfree(bo)(radeon_object.c:85).radeon_bo_clear_surface_regwritesreg->bo = NULL; bo->surface_reg = -1;(lines 667-668) under no lock that the stealer holds β the stealer's resv belongs to a different BO.
Interleavings
- Stealer reads non-NULL
reg->boat :624/:627 or :638; victim destroy executes :667 (reg->bo = NULL) and completeskfree(bo); stealer then executes :628 (old_object->pin_count) or :641/:642 on freed memory β slab UAF read + 4-byte write of0xFFFFFFFFatoffsetof(struct radeon_bo, surface_reg)into a freed kmalloc object (struct radeon_boembedsttm_buffer_object, several hundred bytes β a groomable slab zone reachable with ordinary GEM create/close). - Stealer checks
!reg->boat :624; victim destroy setsreg->bo = NULLin between; stealer readsold_object = NULLat :627 β :628 NULL-deref β kernel panic. Same outcome at :638β:641. - Two stealers picking the same slot corrupt the registry bookkeeping (bounded β no OOB; wrong surface configuration, GPU-side effects only).
Interleaving 2/3 have wide windows (dozens of instructions) and are trivially winnable by repetition; interleaving 1's UAF-write window (:638β:642) is tighter but stable under CPU pinning.
Unprivileged reachability (traced)
radeon_gem_set_tiling_ioctl(radeon_gem.c:503-519) β DRM ioctl, any owner of the GEM handle, no privilege/family check βradeon_bo_set_tiling_flags(radeon_object.c:671) storesRADEON_TILING_SURFACE(0x10) inbo->tiling_flagsafter validating only the EG bankw/bankh/β¦ sub-fields (not the SURFACE bit).- mmap of the handle + page fault β TTM cpu-fault β
radeon_bo_fault_reserve_notify(radeon_object.c:793) βradeon_bo_check_tiling(bo, 0, 0)(:804) β withmem_type == TTM_PL_VRAMandsurface_reg == -1(:757, :766) βradeon_bo_get_surface_reg(:769). (has_movedis never non-zero anywhere in the tree β the only live entry routes areradeon_bo_kmap:289 andfault_reserve_notify:804.) - Occupying all 8 regs is unprivileged: create 8+ VRAM BOs, set SURFACE tiling, fault each. Every 9th registration takes the steal path.
Verification attempt (this run)
- Trigger:
df4584_trigger.cβ occupies all 8 surface regs, then races registration (steal path) against register+destroy (clear path) on two pthreads, 20000 iterations per run. - Built and ran on the audit QEMU guest (DragonFly 6.5-DEVELOPMENT):
cc -O2 -Wall -pthreadβ build OK; run βopen /dev/dri/renderD128: No such file or directory(and card0). The guest has no radeon hardware (only virtio-VGA 0x1111:1234, no DRM attach,/dev/driabsent) and no radeonkms.ko in/boot/kernel(only radeonkmsfw_* firmware modules). The cited code path is therefore live-unreachable on this guest β neither reproducible nor refutable here. - Conclusion: inconclusive on-guest; code-trace certain (the lock protocol violation is provable by inspection β the only locks involved are per-BO reservations and the destroy path holds none of them).
Recommended fix
Serialize registry access with a device-level lock (see fix.diff):
struct lock rdev->surface_regs_lock held across
radeon_bo_get_surface_reg()'s registry walk/steal and across
radeon_bo_clear_surface_reg()'s body. Because the victim's destroy
calls radeon_bo_clear_surface_reg before kfree(bo), holding this
lock in the stealer guarantees the victim cannot be freed mid-steal: if
its destroy already passed the clear, reg->bo is NULL and the slot is
simply free. (Authored post-verification against the read-only tree;
never applied.)
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 validation impossible on this guest: baseline itself is not reproducible (no radeon hardware/driver), so no patched-kernel comparison can be made. fix.diff is inspection-validated only.
fix.diff (git-apply-able vs read-only sys/ tree; never applied); see VERDICT.md 'Recommended fix'
Confirmed kernel references
- sys/dev/drm/radeon/radeon_object.c:601
- sys/dev/drm/radeon/radeon_object.c:609
- sys/dev/drm/radeon/radeon_object.c:621-630
- sys/dev/drm/radeon/radeon_object.c:637-643
- sys/dev/drm/radeon/radeon_object.c:656-669
- sys/dev/drm/radeon/radeon_object.c:69-86
- sys/dev/drm/radeon/radeon_object.c:793-812
- sys/dev/drm/radeon/radeon_gem.c:503-519
Detail
Exploit chain
unpriv user: create 8+ VRAM GEM BOs, GEM_SET_TILING RADEON_TILING_SURFACE, mmap+fault each -> all 8 surface regs busy; thread A faults a 9th SURFACE BO (steal path), thread B creates+faults+closes SURFACE BOs (destroy -> clear) -> A reads reg->bo after B's clear: NULL deref -> panic; or B's kfree lands between A's read (:638) and use (:641/:642) -> UAF read + fixed-value 4-byte write at offsetof(radeon_bo, surface_reg) in freed slab -> groomable toward control (not developed: no HW).
Evidence (decisive lines)
VERDICT.md (interleaving analysis), run.log (open ENOENT + no radeonkms.ko + virtio VGA pciconf), build.log (BUILD_OK), fix.diff (device-lock fix)
PoC changes
Self-authored trigger (no seed existed): self-contained uapi replicas, two pthread racers, render-node-first open; added missing errno.h after first guest compile.
Verified recommended fix
Add device-level rdev->surface_regs_lock held across radeon_bo_get_surface_reg()'s registry walk/steal and radeon_bo_clear_surface_reg()'s body (fix.diff); victim liveness follows because destroy calls clear before kfree.
Verdict
Code-proven race, unverifiable on the audit guest. radeon_bo_get_surface_reg (radeon_object.c:601) walks and steals the device-global rdev->surface_regs[] under ONLY the requesting BO's reservation (assert at :609); the victim BO in reg->bo is dereferenced (:628, :641) and written (:642, surface_reg=-1) with no lock, while the victim's final-unref path radeon_ttm_bo_destroy -> radeon_bo_clear_surface_reg (:656, reg->bo=NULL at :667) -> kfree (:85) runs unlocked. NULL-deref panic (interleaving B) or a 4-byte 0xFFFFFFFF write into freed radeon_bo slab (interleaving A). Unprivileged reach: GEM_SET_TILING stores RADEON_TILING_SURFACE (radeon_gem.c:503), mmap fault -> radeon_bo_fault_reserve_notify (:793) -> check_tiling(:804) -> get_surface_reg with all 8 regs busy. Trigger built cleanly on the guest but the guest has no radeon GPU (virtio VGA 0x1111:1234 only), no /dev/dri, and no radeonkms.ko in /boot/kernel, so the path is live-unreachable there: open('/dev/dri/renderD128') = ENOENT (run.log). Status 'inconclusive' = attempted, hardware absent, neither reproduced nor refuted on-guest.
No comments yet.