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)
PoC verification
Evidence pack
findings/poc/DF-4522 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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->vais 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) callsrb_insert_color_cached(&node->rb, root, false), whose shim macro (rbtree.h:100-101) ignores theleftmostargument and only rebalancesroot->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 withoutrmv, leaving mappings invm->vafor 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 withpath:line.sh static_proof.shβ PASS.df4522_demo.c+Makefileβ kld that includes the realsys/dev/drm/include/linux/rbtree.hand 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β unmodifiedamdgpu.kofull module build (RC=0).fix_build.logβfix.diffapplied in guest/usr/src, incremental rebuild (RC=0, onlyamdgpu_vm.orecompiled, module relinked).fix.diffβ the fix (loop onRB_EMPTY_ROOT+rb_first(), which is a realRB_MINwalk 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): usesrb_first_cached(drm_mm.c:496) which is a realRB_MINwalk β functionally correct, never reads the cache field directly.- generic
interval_tree_generic.hconsumers: lookups go throughrb_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 directrb_leftmostreads.amdgpu_mn.c:412uses undefinedRB_ROOT_CACHEDβ dead file, not in the module build (not insys/dev/drm/amd/amdgpu/MakefileSRCS).
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:
-
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 isamdgpu_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 theleftmostparameter and only runslinux_root_RB_INSERT_COLORonroot->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 forleftmostin the file). -
NULL or garbage? β Deterministically NULL.
vm->vais explicitly initialized:vm->va = LINUX_RB_ROOT_CACHED;(amdgpu_vm.c:3055, insideamdgpu_vm_init), andLINUX_RB_ROOT_CACHEDis(struct rb_root_cached) { {NULL,}, NULL }(rbtree.h:175). The containingstruct amdgpu_fpriviskzalloc'd besides (amdgpu_kms.c:979). So the field cannot be stale garbage. -
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. -
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 invm->vaat teardown (upstream Linux frees exactly these withrbtree_postorder_for_each_entry_safe, amdgpu_vm.c:3317-3318 guarded#ifndef __DragonFly__). Zero iterations β every remainingstruct amdgpu_bo_va_mapping(kmalloc'd M_DRM at amdgpu_vm.c:2526 and :2580) leaks permanently. The precedingdev_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. -
Which function?
amdgpu_vm_fini()(amdgpu_vm.c:3282). There is noamdgpu_vm_cleanup()in this port. Both fini call sites share the walk:amdgpu_kms.c:1015(open error path; tree empty there, moot) andamdgpu_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_closeonttm_eu_reserve_buffers()failure prints"leaking bo va because we fail to reserve bo (%d)"and returns without callingamdgpu_vm_bo_rmv(amdgpu_gem.c:170-175). The comment/name says it: the bo_va (and its tree mappings) are deliberately left foramdgpu_vm_finito 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_cbis 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.kofull build, RC=0 (baseline_build.log); git apply --checkclean on the host repo;- fix applied in guest
/usr/src, incremental rebuild RC=0 with-Werror(fix_build.log; onlyamdgpu_vm.orecompiled,amdgpu.korelinked); - 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 computesmin_entryand passes it torb_insert_color_cached(interval_tree_generic.h:91), which drops it; lookups userb_first_cached(interval_tree_generic.h:60) β correct.i915_gem_userptr.c:181,radeon_mn.c:225(LINUX_RB_ROOT_CACHED) andamdgpu_mn.c:412β the latter referencesRB_ROOT_CACHED, which is defined nowhere insys/, butamdgpu_mn.cis not in the module build (absent fromsys/dev/drm/amd/amdgpu/MakefileSRCS), so it cannot break compilation.i915_gem_userptr.c/radeon_mn.cuse 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_testablefix.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
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:3325
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:3333
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:3055
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:107
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:170
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1073
- sys/dev/drm/include/linux/rbtree.h:100
- sys/dev/drm/include/linux/rbtree.h:104
- sys/dev/drm/linux_compat.c:40
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.
No comments yet.