radeon_vm_get_bos() TOCTOU on vm->max_pde_used β kernel heap out-of-bounds write via racing GEM_VA/CS
Summary
get_bos sizes its array from one read of max_pde_used but the fill loop re-reads it live every iteration. A concurrent GEM_VA on the same fd grows max_pde_used and installs PT BOs one at a time; neither get_bos call site holds vm->mutex. When the bump lands between allocation and loop, idx runs past the array: kernel-pointer stores plus list_add write up to ~190KB past a 2-element allocation (fresh vm per fd, grow-only counter = unbounded retries). Unpriv /dev/dri on cayman+ radeon: heap corruption, privesc-class. Snapshot-the-bound fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4412 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_race.c | β | 5.8 KB | view raw | |
| radeon_abi.h | β | 2.4 KB | view raw | |
| build.sh | β | 112 B | view raw | |
| run.sh | β | 98 B | view raw | |
| build.log | β | 489 B | view raw | |
| run.log | β | 489 B | view raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| README.md | β | 1.5 KB | β raw | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 2.7 KB | view raw | |
| env.txt | β | 585 B | view raw |
DF-4412 β radeon_vm_get_bos() TOCTOU on vm->max_pde_used β heap OOB write
File: sys/dev/drm/radeon/radeon_vm.c (pass 2, GLM 5.3)
Build
On a DragonFly guest with the kernel source tree:
cc -O2 -Wall -pthread -o /tmp/trigger_race DF-4412/trigger_race.c # radeon_abi.h must sit next to the .c (self-contained UABI definitions)
Run
/tmp/trigger_race
Requires a cayman-or-newer radeon KMS device (/dev/dri/card0 with
vm_manager.enabled, i.e. Radeon HD 6900 "Cayman" / Trinity / SI / CIK
class). The racy loop retries with a fresh drm fd each round.
Expected result on capable hardware
Kernel heap corruption: the list[idx] writes and the list_add() in
radeon_vm_get_bos() (radeon_vm.c:149-160) run past the
kvmalloc_array(vm->max_pde_used + 2, β¦) allocation
(radeon_vm.c:135) whenever a concurrent RADEON_GEM_VA ioctl on the
same drm fd grows vm->max_pde_used between the allocation and the
loop's live re-read of the bound. Expected outcome after enough rounds:
kernel panic / slab corruption, or silent corruption of adjacent heap
objects (each overflow slot writes ~5 kernel pointers and links
&list[idx].tv.head into a live list).
Observed on THIS audit guest
/dev/dri does not exist β the QEMU guest has only the std VGA device
(vgapci0 0x1234:0x1111), no radeon GPU. The trigger compiles and runs,
and exits at open("/dev/dri/card0") with ENOENT. Runtime verification
of the race requires radeon hardware that this guest does not have; the
finding is proven by source trace (see VERDICT.md).
DF-4412 β VERDICT
status: not_reproduced (hardware absent) β finding stands by line-level trace; not a false positive
Root cause (certain, by trace)
radeon_vm_get_bos() allocates its array once from a snapshot of
vm->max_pde_used, then uses a live re-read of the same field as the
loop bound:
sys/dev/drm/radeon/radeon_vm.c:135βkvmalloc_array(vm->max_pde_used + 2, sizeof(struct radeon_bo_list), GFP_KERNEL)(bound read #1)sys/dev/drm/radeon/radeon_vm.c:149βfor (i = 0, idx = 1; i <= vm->max_pde_used; i++)(bound re-read every iteration; thelist_add()call and the stores throughlist[idx].robjinside the body force the compiler to reloadvm->max_pde_usedβ they may aliasvm)sys/dev/drm/radeon/radeon_vm.c:153-159βlist[idx].robj = β¦; β¦ list_add(&list[idx++].tv.head, head);β the OOB writes onceidxpasses the allocated element count
vm->max_pde_used is grow-only and updated under vm->mutex in
radeon_vm_bo_set_addr():
radeon_vm.c:535-536βif (eoffset > vm->max_pde_used) vm->max_pde_used = eoffset;radeon_vm.c:573-574βvm->page_tables[pt_idx].bo = pt;(PTs appear one at a time after the bump)
Neither caller of radeon_vm_get_bos() holds vm->mutex:
sys/dev/drm/radeon/radeon_gem.c:569(radeon_gem_va_update_vm) βmutex_lock(&bo_va->vm->mutex)only happens at radeon_gem.c:583, after get_bossys/dev/drm/radeon/radeon_cs.c:191(radeon_cs_parser_bos) β the vm mutex is taken much later atradeon_cs.c:564
Race layout
Thread T2 (CS or GEM_VA on fd F) calls get_bos while thread T1 (GEM_VA on
the same fd F, same radeon_vm) maps a BO near the top of the VA space:
- T2:
kvmalloc_array(0 + 2, 48)β 2-element array (fresh vm,max_pde_used == 0because only a low mapping exists) - T1:
vm->max_pde_used = 4095+ starts fillingvm->page_tables[i].boone by one (defaultvm_size=8βmax_pfn = 8<<18 = 2MPFNs β 4095 PDEs at block_size 9) - T2: loop bound now evaluates
i <= 4095; every non-NULLpage_tables[i].bowriteslist[idx++]β up to ~4096 slots Γ 48 bytes β 190 KB past a 2-element allocation, each slot containing kernel pointers and an in-listlist_head.
vm->max_pde_used never shrinks, so each round needs a fresh drm fd
(attacker opens/closes /dev/dri/card0 β every open creates a fresh
radeon_vm with max_pde_used == 0). Attacker retries unboundedly.
Why not reproduced here
This QEMU guest has no radeon GPU (/dev/dri absent; the only VGA
device is the emulated 0x1234:0x1111 std VGA β see env.txt). The
trigger builds cleanly (gcc 8.3, DF 6.5-DEVELOPMENT, see build.log) and
runs; it exits at open("/dev/dri/card0"): ENOENT (run.log). The race
itself needs a cayman+ radeon device with KMS, which no QEMU target can
provide (no radeon emulation exists upstream in QEMU).
Not a false positive: every element of the chain is unconditional source fact; the only untested quantity is the race window, which is a timing property, not a correctness one. Severity High (kernel heap OOB write from an unprivileged DRI client), exploitability "likely".
Exploit chain (sketch, if the class permits)
win race β OOB write of struct radeon_bo_list slots
({struct radeon_bo *robj; u32 preferred, allowed; struct ttm_validate_buffer tv {struct ttm_buffer_object *bo; struct list_head head; bool shared}; u64 tiling_flags})
plus list_add() of an OOB list_head into p->validated β corrupt
adjacent slab objects (on DFly, kvmalloc_array β kmalloc β kmalloc
zones) β target a zone-mated victim object (e.g. file/proc structures)
β forged pointer / refcount pivot β uid=0. The written values are
partially attacker-influenced (bo domain bits, bo kernel pointers).
Recommended fix
Snapshot the bound into a local before allocating (no lock needed, kills
the TOCTOU). 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 validation impossible on this guest: the bug itself cannot be triggered (no radeon GPU), so baseline vs patched comparison has no observable. fix.diff is line-accurate against the traced root cause and applies to sys/dev/drm/radeon/radeon_vm.c.
fix.diff (snapshot of max_pde_used for both alloc and loop bound)
Confirmed kernel references
Detail
Exploit chain
win TOCTOU race (fresh drm fd per round) -> list[idx]/list_add() in radeon_vm_get_bos() write ~48-byte kernel-pointer slots past the kvmalloc_array -> corrupt adjacent kmalloc-zone objects -> victim-object pivot -> uid=0 (sketch, untested without HW)
Evidence (decisive lines)
VERDICT.md (race layout + line cites); run.log (device-absent); fix.diff (snapshot fix)
PoC changes
initial draft used kernel uapi include (broken chain on DFly: uapi/linux/types.h wants asm/int-ll64.h); replaced with self-contained radeon_abi.h; USE_VM moved to a RADEON_CHUNK_ID_FLAGS chunk (drm_radeon_cs_chunk has no flags member); -pthread added
Verified recommended fix
Snapshot vm->max_pde_used into a local before kvmalloc_array and use the snapshot as the loop bound
Verdict
Kernel heap OOB write by trace (radeon_vm.c:135 snapshot vs :149 live re-read of vm->max_pde_used; neither call site holds vm->mutex - radeon_gem.c:569 and radeon_cs.c:191 both precede the mutex_lock at radeon_gem.c:583 / radeon_cs.c:564), but the audit guest has no radeon GPU (/dev/dri absent, QEMU std VGA only) so the race cannot be exercised at runtime. Trigger compiles and runs, exits at open(/dev/dri/card0)=ENOENT (run.log). Analysis-classified impact: kernel heap corruption from an unprivileged DRI client (High); not a false positive - every element of the chain is unconditional source fact, only the timing is untested.
No comments yet.