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

amdgpu_vm_fini's DragonFly cleanup loop iterates on vm->va.rb_leftmost, which the DFly rb_root_cached shim never maintains: zero iterations, all leftover VM mappings leak

Summary

The #else __DragonFly__ fini walk loops 'while (vm->va.rb_leftmost)' - but the shim's rb_insert_color_cached/rb_erase_cached ignore the cache field entirely, vm->va is zero-initialized, and the ONLY rb_leftmost references in all of sys/ are this loop's own reads. The loop runs exactly ZERO iterations: every mapping still in the tree at fini leaks its ~80-byte M_DRM kmalloc - notably the mappings amdgpu_gem_object_close deliberately leaks on reserve-failure ('we fail to reserve bo', relying on this fini loop to reclaim them) and any still-active-bo teardown state. Proven on the running guest kernel with the verbatim shim + verbatim loop (0 freed / 64 leaked vs fixed loop 64/64); fix build-validated (amdgpu.ko RC=0 -Werror). Requires AMD hardware at runtime. rb_first()-based loop fix. The infinite-loop half of the lead was refuted (field deterministically NULL).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4522 Β· 14 files
FileTypeDescriptionSize
README.md β€” 5.2 KB ↓ raw
VERDICT.md β€” 8.9 KB ↓ raw
df4522_demo.c β€” 6.0 KB view raw
Makefile β€” 101 B ↓ download
static_proof.sh β€” 2.6 KB view raw
build.sh β€” 125 B view raw
run.sh β€” 169 B view raw
build.log β€” 5.8 KB view raw
run.log β€” 368 B view raw
baseline_build.log β€” 1.2 MB ↓ download
fix_build.log β€” 8.6 KB view raw
env.txt β€” 1.8 KB view raw
fix.diff β€” 1.0 KB view raw
verdict.json β€” 6.2 KB view raw

DF-4522 β€” amdgpu_vm_fini's DragonFly cleanup loop is dead code (rb_leftmost never maintained) β†’ leaked VM mappings

File: sys/dev/drm/amd/amdgpu/amdgpu_vm.c:3319-3334 Class: kernel memory leak (driver-local, M_DRM) / porting defect β€” CWE-401 Severity: Low (requires AMD GPU; abnormal-but-real teardown paths) Confidence: certain (static proof + on-kernel semantic demo) Status: runtime not testable on the audit guest (no AMD GPU); mechanism proven on the running kernel; fix compile-validated (module builds).

What is broken

The DragonFly branch of amdgpu_vm_fini() drains leftover mappings with:

while (vm->va.rb_leftmost) {                       /* amdgpu_vm.c:3325 */
        mapping = container_of((void *)vm->va.rb_leftmost, ...);
        list_del(&mapping->list);
        amdgpu_vm_it_remove(mapping, &vm->va);
        kfree(mapping);
}

But on this port struct rb_root_cached::rb_leftmost (sys/dev/drm/include/linux/rbtree.h:64-67) is never written by anyone:

  • vm->va is zero-initialized (vm->va = LINUX_RB_ROOT_CACHED, amdgpu_vm.c:3055; LINUX_RB_ROOT_CACHED = { {NULL,}, NULL }, rbtree.h:175);
  • the insert path amdgpu_vm_it_insert() (amdgpu_vm.c:107-127) calls rb_insert_color_cached(&node->rb, root, false), whose shim macro (rbtree.h:100-101) ignores the leftmost argument and only rebalances root->rb_root;
  • amdgpu_vm_it_remove() β†’ rb_erase_cached (rbtree.h:102-103) likewise;
  • grep -rn rb_leftmost sys/ β†’ the ONLY references in the whole kernel tree are the two read sites of this loop (amdgpu_vm.c:3325-3326).

So vm->va.rb_leftmost is deterministically NULL for the VM's entire lifetime, the while body never executes, and every mapping still in vm->va when amdgpu_vm_fini() runs is leaked (kfree at :3333 never runs; dev_err("still active bo inside vm") at :3313-3315 fires first).

This is NOT an infinite loop and NOT a wild pointer deref β€” the field is NULL, not garbage (explicit init + kzalloc'd fpriv, amdgpu_kms.c:979).

When it leaks

Normal GEM handle close removes mappings (amdgpu_gem_object_close β†’ amdgpu_vm_bo_rmv, amdgpu_gem.c:185 β†’ amdgpu_vm.c:2847-2859), so a perfectly clean exit empties the tree. The leak hits the paths upstream Linux deliberately handles in this same loop:

  • amdgpu_gem_object_close() reserve failure β€” amdgpu_gem.c:170-175 prints "leaking bo va because we fail to reserve bo" and returns without rmv, leaving mappings in vm->va for fini to clean up (upstream frees them here; this port leaks them);
  • any driver/teardown state that triggers the upstream "still active bo inside vm" condition (amdgpu_vm.c:3313);
  • the KFD destroy callback is an empty stub on this port (amdgpu_amdkfd.c:534-537) β€” no additional cleanup happens there.

Leaked objects are struct amdgpu_bo_va_mapping (~80 bytes, kmalloc M_DRM at amdgpu_vm.c:2526/2580), one per mapping, repeatable per VM open/ close cycle by any user with access to an AMD GPU's /dev/dri/* node.

Pack contents / how to reproduce

  • static_proof.sh β€” airtight source-level proof (runs on the host): 5 assertions, each with path:line. sh static_proof.sh β†’ PASS.
  • df4522_demo.c + Makefile β€” kld that includes the real sys/dev/drm/include/linux/rbtree.h and runs the verbatim amdgpu interval-tree shim (amdgpu_vm.c:100-127) plus the verbatim fini loop (amdgpu_vm.c:3325-3334) against a populated tree on the running kernel, then the fixed loop on an identical tree.
  • build.sh / run.sh β€” exact commands used against the guest.
  • build.log β€” demo kld build (guest, cc 8.3, -Werror, clean).
  • run.log β€” kldload output, the decisive lines: DF4522: inserted 64 mappings per tree; tree_nonempty=1 rb_leftmost=0 rb_first=0xfffff8008d8a20b8 DF4522: BUGGED fini loop: iterations=0 freed=0 LEAKED=64 (tree still nonempty=1) DF4522: FIXED fini loop: iterations=64 freed=64 LEAKED=0 (tree empty=1)
  • baseline_build.log β€” unmodified amdgpu.ko full module build (RC=0).
  • fix_build.log β€” fix.diff applied in guest /usr/src, incremental rebuild (RC=0, only amdgpu_vm.o recompiled, module relinked).
  • fix.diff β€” the fix (loop on RB_EMPTY_ROOT + rb_first(), which is a real RB_MIN walk on this port, rbtree.h:97/104).
  • env.txt β€” guest uname / kldstat / cc version.

The demo cannot and does not claim a runtime hit on amdgpu_vm_fini itself: the guest has no AMD GPU, so amdgpu.ko never attaches. The mechanism (shim never maintains rb_leftmost β†’ verbatim loop idles) is proven on the running kernel with the real header.

Not affected (checked)

  • drm_mm.c (DF-4521's file): uses rb_first_cached (drm_mm.c:496) which is a real RB_MIN walk β€” functionally correct, never reads the cache field directly.
  • generic interval_tree_generic.h consumers: lookups go through rb_first_cached β†’ RB_MIN β€” correct; the un-maintained cache only costs the O(1) optimization.
  • i915_gem_userptr.c:181, radeon_mn.c:225: same generic-shim pattern, no direct rb_leftmost reads.
  • amdgpu_mn.c:412 uses undefined RB_ROOT_CACHED β€” dead file, not in the module build (not in sys/dev/drm/amd/amdgpu/Makefile SRCS).
VERDICT.md
↓ download raw

DF-4522 VERDICT β€” verified real (dead cleanup loop / mapping leak); mechanism proven on-kernel, runtime gated on absent AMD hardware

Verdict: REAL BUG, proven statically (certain) and empirically at shim level on the running DragonFly kernel. The amdgpu_vm_fini() DragonFly cleanup walk is dead code; leftover VM mappings leak. NOT an infinite loop / wild deref (the lead's worst-case hypothesis is refuted: the field is deterministically NULL, never garbage).

1. The lead, and what is actually true

Lead (from the drm_mm.c pass-2, DF-4521 negative notes): "amdgpu_vm.c:3325-3326 loops on vm->va.rb_leftmost, but amdgpu's own DFly interval-tree insert never sets rb_leftmost … the fini/teardown walk may loop forever (if rb_leftmost is NULL/garbage) or leak mappings."

Verified point by point:

  1. Nothing sets vm->va.rb_leftmost β€” CONFIRMED. rg -n 'rb_leftmost' sys/ yields exactly two hits, both the reads inside the fini loop itself (amdgpu_vm.c:3325, amdgpu_vm.c:3326). No writer exists anywhere in the kernel tree. The insert path is amdgpu_vm_it_insert (amdgpu_vm.c:107-127) β†’ rb_insert_color_cached(&node->rb, root, false); the shim (sys/dev/drm/include/linux/rbtree.h:100-101) discards the leftmost parameter and only runs linux_root_RB_INSERT_COLOR on root->rb_root. amdgpu_vm_it_remove β†’ rb_erase_cached (rbtree.h:102-103) likewise never touches the cache field. sys/sys/tree.h (the underlying RB implementation) has no leftmost-cache concept at all (no match for leftmost in the file).

  2. NULL or garbage? β€” Deterministically NULL. vm->va is explicitly initialized: vm->va = LINUX_RB_ROOT_CACHED; (amdgpu_vm.c:3055, inside amdgpu_vm_init), and LINUX_RB_ROOT_CACHED is (struct rb_root_cached) { {NULL,}, NULL } (rbtree.h:175). The containing struct amdgpu_fpriv is kzalloc'd besides (amdgpu_kms.c:979). So the field cannot be stale garbage.

  3. Infinite loop? β€” NO. while (vm->va.rb_leftmost) with the field NULL-forever executes zero iterations, always. The lead's "loop forever" hypothesis is dead: there is no non-NULL value to loop on, and the loop body (which removes nodes) never runs to re-read a stale value.

  4. Leak? β€” YES. The loop body at amdgpu_vm.c:3331-3333 (list_del, amdgpu_vm_it_remove, kfree(mapping)) is the only cleanup for mappings still in vm->va at teardown (upstream Linux frees exactly these with rbtree_postorder_for_each_entry_safe, amdgpu_vm.c:3317-3318 guarded #ifndef __DragonFly__). Zero iterations β‡’ every remaining struct amdgpu_bo_va_mapping (kmalloc'd M_DRM at amdgpu_vm.c:2526 and :2580) leaks permanently. The preceding dev_err(adev->dev, "still active bo inside vm") (amdgpu_vm.c:3313-3315) fires in exactly the leaking cases β€” the kernel even tells you it is about to leak.

  5. Which function? amdgpu_vm_fini() (amdgpu_vm.c:3282). There is no amdgpu_vm_cleanup() in this port. Both fini call sites share the walk: amdgpu_kms.c:1015 (open error path; tree empty there, moot) and amdgpu_kms.c:1073 (amdgpu_driver_postclose_kms, the real teardown).

2. Reachability of the leak

The normal close path empties the tree: GEM handle close β†’ amdgpu_gem_object_close (amdgpu_gem.c:152) β†’ amdgpu_vm_bo_rmv (amdgpu_gem.c:185) β†’ mappings unlinked at amdgpu_vm.c:2847-2859. So the dead loop is harmless on perfectly-clean exits. It leaks on the paths the upstream cleanup exists for:

  • Reserve-failure path, by design: amdgpu_gem_object_close on ttm_eu_reserve_buffers() failure prints "leaking bo va because we fail to reserve bo (%d)" and returns without calling amdgpu_vm_bo_rmv (amdgpu_gem.c:170-175). The comment/name says it: the bo_va (and its tree mappings) are deliberately left for amdgpu_vm_fini to reclaim. Linux reclaims them in this very loop; this port leaks every one.
  • Any "still active bo inside vm" condition (upstream's dev_err): driver bug or unclean teardown states leave mappings behind.
  • KFD does not help: amdgpu_amdkfd_gpuvm_destroy_cb is an empty stub in this port (amdgpu_amdkfd.c:534-537).

Attacker/impact model: any local user with access to an AMD GPU's DRM node can create thousands of mappings per VM and, via the reserve-fail or other abnormal exit paths, leak them all; repeat per open/close cycle. Bounded by mapping count per cycle (~80 bytes each), permanent, unbounded across cycles. No memory-safety consequence (no UAF, no double-free: the leaked nodes are simply never referenced again).

3. Empirical proof on the running kernel (mechanism level)

df4522_demo.c (kld) includes the real shim header (sys/dev/drm/include/linux/rbtree.h), runs the verbatim amdgpu interval-tree shim (amdgpu_vm.c:100-127) and the verbatim fini loop (amdgpu_vm.c:3325-3334) on a 64-node tree, then the fixed loop on an identical tree. Guest: DragonFly 6.5-DEVELOPMENT x86_64 (env.txt). Decisive output (run.log):

DF4522: inserted 64 mappings per tree; tree_nonempty=1 rb_leftmost=0 rb_first=0xfffff8008d8a20b8
DF4522: BUGGED  fini loop: iterations=0 freed=0 LEAKED=64 (tree still nonempty=1)
DF4522: FIXED  fini loop: iterations=64 freed=64 LEAKED=0 (tree empty=1)

Tree populated, rb_first() (real RB_MIN walk) finds nodes, yet rb_leftmost is NULL β†’ the verbatim loop idles and leaks 64/64; the fix loop drains 64/64. kldload/kldunload clean, guest healthy.

4. Why status is not_testable (not reproduced)

The audit guest has no AMD GPU (kldstat: kernel + ehci/xhci only, env.txt); amdgpu.ko never attaches, so the real amdgpu_vm_fini() path cannot be driven at runtime. Per the honest-status table this is not_testable / reproduced=0 / impact=none for the poc_results row β€” the mechanism-level demo and the airtight static proof carry the certainty instead (confidence: certain for the dead-code property itself).

5. Fix β€” validated at module-build level

fix.diff replaces the dead loop with a live one that walks the real tree (rb_first() = RB_MIN, rbtree.h:97; both macros already used in this file at runtime: rb_first_cached at amdgpu_vm.c:77 via amdgpu_vm_it_iter_first, RB_EMPTY_ROOT at amdgpu_vm.c:3313):

-   while (vm->va.rb_leftmost) {
-       mapping = container_of((void *)vm->va.rb_leftmost, struct amdgpu_bo_va_mapping, rb);
+   while (!RB_EMPTY_ROOT(&vm->va.rb_root)) {
+       mapping = rb_entry(rb_first(&vm->va.rb_root),
+                  struct amdgpu_bo_va_mapping, rb);

Validation performed on the guest:

  • baseline: unmodified amdgpu.ko full build, RC=0 (baseline_build.log);
  • git apply --check clean on the host repo;
  • fix applied in guest /usr/src, incremental rebuild RC=0 with -Werror (fix_build.log; only amdgpu_vm.o recompiled, amdgpu.ko relinked);
  • guest source restored to pristine (sha256 match) after the build;
  • fixed-loop semantics proven on-kernel by the demo (64/64 freed).

An equally valid alternative (closer to upstream) is deleting the #ifndef __DragonFly__ special-case entirely and using rbtree_postorder_for_each_entry_safe, which this port's rbtree.h already provides (rbtree.h:135-140) β€” the porter's comment "the RB iterator can not be used" is stale. The minimal diff above was preferred.

6. Other consumers with the same problem β€” none

rb_leftmost direct readers in sys/: only amdgpu_vm.c:3325-3326 (proof step [1] in static_proof.sh). All other rb_root_cached users go through rb_first_cached() (rbtree.h:104), which is a real RB_MIN walk and therefore correct even with the cache un-maintained β€” they merely lose the O(1) fast path:

  • drm_mm.c β€” rb_insert_color_cached (drm_mm.c:227, 271), rb_erase_cached (drm_mm.c:293), rb_first_cached(&mm->holes_size) (drm_mm.c:496): correct. (Consistent with DF-4521's pass-2 negative notes.)
  • interval_tree_generic.h (OpenBSD/FreeBSD-derived shim) β€” insert computes min_entry and passes it to rb_insert_color_cached (interval_tree_generic.h:91), which drops it; lookups use rb_first_cached (interval_tree_generic.h:60) β†’ correct.
  • i915_gem_userptr.c:181, radeon_mn.c:225 (LINUX_RB_ROOT_CACHED) and amdgpu_mn.c:412 β€” the latter references RB_ROOT_CACHED, which is defined nowhere in sys/, but amdgpu_mn.c is not in the module build (absent from sys/dev/drm/amd/amdgpu/Makefile SRCS), so it cannot break compilation. i915_gem_userptr.c/radeon_mn.c use the generic-shim lookup pattern β†’ functionally correct.

Root cause home: shared shim (sys/dev/drm/include/linux/rbtree.h), but the only defect consumer is amdgpu_vm.c's direct field read; filed under the consumer per the tasking, shim noted.

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

fix.diff is validated at module-build level (baseline amdgpu.ko RC=0; patched amdgpu_vm.o recompiles clean under -Werror, module relinks, fix_build.log) and at semantics level on the running kernel (demo: fixed loop frees 64/64 on an identical tree where the verbatim loop freed 0). Full runtime validation on real amdgpu_vm_fini is impossible on this guest (no AMD GPU), hence not_testable rather than fixed. Guest source restored to pristine (sha256 7005b972...) after the patched build.

fix_build.log (RC=0, amdgpu_vm.c recompile + amdgpu.ko relink); run.log 'FIXED fini loop: iterations=64 freed=64 LEAKED=0'; git apply --check clean on host repo
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64 (module-build validation only)

Confirmed kernel references

Detail

Evidence (decisive lines)

static_proof.sh (5 assertions, PASS, host); run.log lines 'DF4522: BUGGED fini loop: iterations=0 freed=0 LEAKED=64 (tree still nonempty=1)' and 'DF4522: FIXED fini loop: iterations=64 freed=64 LEAKED=0'; df4522_demo.c (verbatim shim amdgpu_vm.c:100-127 + verbatim loop amdgpu_vm.c:3325-3334); build.log (kld build, -Werror clean); baseline_build.log + fix_build.log (amdgpu.ko RC=0 unmodified and with fix.diff); VERDICT.md full trace; env.txt (guest has no AMD GPU: kldstat kernel+ehci/xhci only)

PoC changes

Lead arrived as a task_log note, no seed PoC. Built from scratch: (a) static_proof.sh grep-level proof over sys/; (b) df4522_demo.c kld -- includes the REAL sys/dev/drm/include/linux/rbtree.h and RB_GENERATEs linux_root like linux_compat.c:40 (drm.ko unloaded on guest, no symbol clash); carries the shim and fini loop VERBATIM from amdgpu_vm.c with three marked adaptations (reduced mapping struct, private MALLOC_DEFINE, list_del omitted); (c) guest build quirks solved: base64(1) absent -> openssl base64 transfer; -j8 module build races forwarder-header generation -> use -j1; single-object make target missing forwarder dir -> full make only.

Verified recommended fix

Replace the dead rb_leftmost walk in amdgpu_vm_fini with a real tree walk: while (!RB_EMPTY_ROOT(&vm->va.rb_root)) { mapping = rb_entry(rb_first(&vm->va.rb_root), struct amdgpu_bo_va_mapping, rb); ... } (see fix.diff); alternatively restore the upstream postorder iterator, which this port's rbtree.h already provides (rbtree.h:135).

Verdict

REAL BUG, proven two ways, runtime-gated on absent hardware. (1) Static proof (static_proof.sh, PASS): the only rb_leftmost references in all of sys/ are the two READ sites of the amdgpu_vm_fini cleanup loop (amdgpu_vm.c:3325-3326); the shim's rb_insert_color_cached/rb_erase_cached (sys/dev/drm/include/linux/rbtree.h:100-103) never write the cache field and sys/tree.h has no leftmost concept, while vm->va is deterministically zero-initialized (amdgpu_vm.c:3055, LINUX_RB_ROOT_CACHED). Therefore the DragonFly fini loop executes ZERO iterations and its list_del/amdgpu_vm_it_remove/kfree body (amdgpu_vm.c:3331-3333) is dead code: every struct amdgpu_bo_va_mapping still in the tree at amdgpu_vm_fini leaks permanently (~80B M_DRM each, amdgpu_vm.c:2526/2580). The lead's infinite-loop hypothesis is REFUTED (field is NULL-forever, never garbage); the leak hypothesis is CONFIRMED. Reachable via the deliberate reserve-failure leak path in amdgpu_gem_object_close (amdgpu_gem.c:170-175 leaves mappings for fini to reclaim) and any 'still active bo inside vm' condition (amdgpu_vm.c:3313). (2) On-kernel semantic demo (run.log): a kld built against the real rbtree.h running the verbatim amdgpu shim + verbatim fini loop shows 'BUGGED fini loop: iterations=0 freed=0 LEAKED=64' with the tree still nonempty, while the fixed loop drains 64/64. Status is not_testable only because the guest has no AMD GPU (amdgpu.ko cannot attach); impact recorded 'none' per the honest-status table for the runtime row, but the defect's real-world effect is a repeatable kernel memory leak on AMD GPU systems, filed as Low severity / CWE-401.