# 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:

```c
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).
