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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4584 Β· 11 files
FileTypeDescriptionSize
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 = -1 on freed radeon_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. See env.txt, run.log.

Contents

  • df4584_trigger.c β€” race trigger (two pthreads: register vs destroy)
  • build.sh / run.sh β€” exact commands
  • build.log / run.log β€” full guest output (untrimmed)
  • env.txt β€” guest environment proof
  • VERDICT.md β€” full narrative + interleaving analysis
  • fix.diff β€” device-lock fix (git-apply-able; not applied to sys/)
  • manifest.json / verdict.json β€” machine-readable results
VERDICT.md
↓ download raw

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 with lockdep_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_reg writes reg->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

  1. Stealer reads non-NULL reg->bo at :624/:627 or :638; victim destroy executes :667 (reg->bo = NULL) and completes kfree(bo); stealer then executes :628 (old_object->pin_count) or :641/:642 on freed memory β†’ slab UAF read + 4-byte write of 0xFFFFFFFF at offsetof(struct radeon_bo, surface_reg) into a freed kmalloc object (struct radeon_bo embeds ttm_buffer_object, several hundred bytes β€” a groomable slab zone reachable with ordinary GEM create/close).
  2. Stealer checks !reg->bo at :624; victim destroy sets reg->bo = NULL in between; stealer reads old_object = NULL at :627 β†’ :628 NULL-deref β†’ kernel panic. Same outcome at :638β†’:641.
  3. 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) stores RADEON_TILING_SURFACE (0x10) in bo->tiling_flags after 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) β†’ with mem_type == TTM_PL_VRAM and surface_reg == -1 (:757, :766) β†’ radeon_bo_get_surface_reg (:769). (has_moved is never non-zero anywhere in the tree β€” the only live entry routes are radeon_bo_kmap :289 and fault_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/dri absent) 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).

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_testable
baseline no→ patch + rebuild →patched clean

Fix 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'
↓ fix.diffper-fix-DF-4584

Confirmed kernel references

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.