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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4412 Β· 11 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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; the list_add() call and the stores through list[idx].robj inside the body force the compiler to reload vm->max_pde_used β€” they may alias vm)
  • sys/dev/drm/radeon/radeon_vm.c:153-159 β€” list[idx].robj = …; … list_add(&list[idx++].tv.head, head); β€” the OOB writes once idx passes 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:

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:

  1. T2: kvmalloc_array(0 + 2, 48) β†’ 2-element array (fresh vm, max_pde_used == 0 because only a low mapping exists)
  2. T1: vm->max_pde_used = 4095 + starts filling vm->page_tables[i].bo one by one (default vm_size=8 β†’ max_pfn = 8<<18 = 2M PFNs β†’ 4095 PDEs at block_size 9)
  3. T2: loop bound now evaluates i <= 4095; every non-NULL page_tables[i].bo writes list[idx++] β†’ up to ~4096 slots Γ— 48 bytes β‰ˆ 190 KB past a 2-element allocation, each slot containing kernel pointers and an in-list list_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).

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

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

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.