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

radeon_vm_bo_set_addr() commits the mapping before page-table allocation β€” allocation failure leaves NULL page tables and the next update panics in radeon_vm_update_ptes()

Summary

set_addr inserts bo_va into the trees BEFORE the PT allocation loop; the error returns leave the mapping committed with page_tables[pt_idx].bo==NULL. Any subsequent update on that bo_va - including the GEM_VA UNMAP cleanup itself, a CS reloc, or an eviction invalidate - walks the range and dereferences pt->tbo.resv with pt==NULL: guaranteed panic once TTM cannot satisfy the 4KB VRAM PT under memory pressure (unpriv GEM_CREATE spam beats eviction). Rollback-on-failure fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4413 Β· 11 files
FileTypeDescriptionSize
trigger_nullpt.c β€” 4.3 KB view raw
radeon_abi.h β€” 2.4 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 100 B view raw
run.log β€” 576 B view raw
full.log β€” 493 B view raw
VERDICT.md β€” 3.5 KB ↓ raw
README.md β€” 1.4 KB ↓ raw
fix.diff β€” 1.5 KB view raw
verdict.json β€” 2.4 KB view raw
env.txt β€” 585 B view raw

DF-4413 β€” radeon_vm_bo_set_addr() commits mapping before PT allocation; failure leaves NULL page tables β†’ NULL-deref panic in radeon_vm_update_ptes()

Build

cc -O2 -Wall -o /tmp/trigger_nullpt DF-4413/trigger_nullpt.c
# radeon_abi.h must sit next to the .c

Run

/tmp/trigger_nullpt

Requires a cayman-or-newer radeon KMS device.

Expected result on capable hardware

  1. The program fills VRAM/GTT (and drives TTM into system memory) until the 4 KB page-table radeon_bo_create() inside radeon_vm_bo_set_addr() fails.
  2. GEM_VA MAP at 0x100000 returns an error β€” but the mapping has already been inserted into vm->va and vm->cleared (radeon_vm.c:519-526) and the error paths at radeon_vm.c:554-560 return without unwinding it.
  3. The follow-up GEM_VA UNMAP (or any CS referencing the BO, or an eviction-driven invalidate) walks the full recorded range in radeon_vm_update_ptes(), hits vm->page_tables[pt_idx].bo == NULL and dereferences it at radeon_vm.c:833 (pt->tbo.resv) β†’ kernel panic (NULL+offset read in kernel mode).

Success criterion: Fatal kernel mode data access (fault va 0x…) / panic backtrace containing radeon_vm_update_ptes / radeon_sync_resv.

Observed on THIS audit guest

No radeon GPU (/dev/dri absent; std VGA only). Trigger builds and runs, exits at device open (see run.log). The flaw is proven by trace in VERDICT.md.

VERDICT.md
↓ download raw

DF-4413 β€” VERDICT

status: not_reproduced (hardware absent) β€” finding certain by trace; not a false positive

Root cause (certain)

radeon_vm_bo_set_addr() records the mapping before it allocates the backing page tables, and its page-table allocation loop has error returns that leave the half-initialized mapping committed:

  1. sys/dev/drm/radeon/radeon_vm.c:519-526 β€” under vm->status_lock, sets bo_va->it.start/it.last, adds bo_va to vm->cleared, then interval_tree_insert(&bo_va->it, &vm->va) β€” mapping committed.
  2. radeon_vm.c:538 β€” radeon_bo_unreserve(bo_va->bo).
  3. radeon_vm.c:541-575 β€” the PT allocation loop: * radeon_vm.c:550-555 β€” radeon_bo_create(...) fails β†’ return r; * radeon_vm.c:557-561 β€” radeon_vm_clear_bo() fails β†’ radeon_bo_unref(&pt); return r; Neither path removes the committed mapping β†’ bo_va sits in vm->va/vm->cleared with vm->page_tables[pt_idx].bo == NULL for the un-allocated tail (or for every index if the first PT failed).
  4. radeon_gem_va_ioctl() (radeon_gem.c:690-695) only calls radeon_gem_va_update_vm() if (!r) β€” the failed MAP is reported to userspace as an error while the kernel state keeps the mapping.

Crash sinks (every subsequent update of that bo_va)

radeon_vm_bo_update() passes bo_va->it.start, bo_va->it.last + 1 to radeon_vm_update_ptes() (radeon_vm.c:1012-1014); the walk does:

827:    uint64_t pt_idx = addr >> radeon_vm_block_size;
828:    struct radeon_bo *pt = vm->page_tables[pt_idx].bo;   /* NULL */
833:    radeon_sync_resv(rdev, &ib->sync, pt->tbo.resv, true); /* panic */

(The pt_idx itself is in-bounds β€” the page_tables[] array is sized num_pdes in radeon_vm_init β€” the stored value is NULL.)

Reachable from three unprivileged paths after the failed MAP:

  • GEM_VA UNMAP of the same BO β€” radeon_gem.c:586 β†’ radeon_vm_clear_freed() (radeon_vm.c:1060) β†’ radeon_vm_bo_update(NULL) β†’ radeon_vm_update_ptes β†’ NULL deref. The "cleanup" unmap is itself the trigger (deterministic).
  • CS referencing the BO β€” radeon_cs.c:527 β†’ same walk.
  • Eviction/invalidate β€” radeon_vm_bo_invalidate() + radeon_vm_clear_invalids() (radeon_vm.c:1098) β†’ same walk.

Precondition realism

PT allocation failure requires TTM to be unable to provide one 4 KB VRAM BO even after eviction: VRAM+GTT+system under pressure (unprivileged GEM_CREATE spam), or radeon_vm_clear_bo()'s radeon_ib_get() failing under SA-pool pressure / GPU reset. The error path exists precisely because the authors expected these failures β€” but its state on return is inconsistent, and the very next touch panics.

Why not reproduced here

No radeon GPU on the audit guest (std VGA only; /dev/dri absent). Trigger compiles and runs, exits at open("/dev/dri/card0") (run.log). The chain above is unconditional source fact; only the memory-pressure setup is guest-hardware dependent. Severity Medium (local kernel panic / DoS from an unprivileged DRI client).

On failure inside the PT loop, roll the mapping back out of vm->va/vm->cleared (restore it.start/it.last = 0) before returning. 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 (no radeon GPU; bug not triggerable). fix.diff adds an error_rollback path that removes the committed interval under the proper locks.

fix.diff (error_rollback: interval_tree_remove + status_lock-protected reset)
↓ fix.diffper-fix-DF-4413

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (commit-before-alloc trace + three crash sinks); run.log (device-absent); fix.diff (rollback on failure)

PoC changes

kernel uapi include replaced by self-contained radeon_abi.h (uapi chain broken for userspace on DFly)

Verified recommended fix

Roll the mapping back out of vm->va/vm->cleared before returning the allocation error

Verdict

Partial-commit bug certain by trace: radeon_vm_bo_set_addr() inserts the mapping into vm->va/vm->cleared (radeon_vm.c:519-526) before the PT loop, and the error returns at radeon_vm.c:554-560 leave it there with vm->page_tables[pt_idx].bo == NULL; the next update (GEM_VA UNMAP cleanup, CS reloc, eviction invalidate) walks radeon_vm_update_ptes() which dereferences pt->tbo.resv at radeon_vm.c:833 with pt==NULL -> kernel panic. The audit guest has no radeon GPU (/dev/dri absent, std VGA) so the panic cannot be triggered at runtime; trigger compiles and runs, exits at open()=ENOENT (run.log). Analysis-classified impact: local kernel panic/DoS from an unprivileged DRI client (Medium).