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

DragonFly rbtree shim never maintains rb_root_cached.rb_leftmost β€” amdgpu_vm_fini's mapping-cleanup loop is dead code, leaking every mapping left in the interval tree at teardown

Summary

The DFly shim's rb_insert_color_cached discards its leftmost argument and rb_erase_cached never touches root->rb_leftmost; the initializer sets it NULL and nothing ever writes it. amdgpu_vm_fini's __DragonFly__ cleanup 'while (vm->va.rb_leftmost)' therefore never executes: any mapping still in vm->va at teardown leaks, including the dev_err('still active bo inside vm') case. Upstream uses rbtree_postorder_for_each_entry_safe - the DFly replacement is the divergence. Normally masked because GEM close removes mappings; kernel-internal/KFD teardown orderings can hit it. The same broken macro contract is a latent landmine for any other rb_leftmost user under this shim.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3865 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
build.sh β€” 101 B view raw
run.sh β€” 112 B view raw
build.log β€” 35 B view raw
run.log β€” 137 B view raw
env.txt β€” 483 B view raw
fix.diff β€” 561 B view raw
manifest.json β€” 950 B view raw
verdict.json β€” 2.4 KB view raw

DF-3865 β€” DragonFly rbtree shim never maintains rb_root_cached.rb_leftmost; amdgpu_vm_fini()'s mapping-cleanup loop is dead code β†’ permanent mapping leak

Files: sys/dev/drm/amd/amdgpu/amdgpu_vm.c (fini loop), sys/dev/drm/include/linux/rbtree.h (shim)

Root cause

The DFly compat shim implements the Linux leftmost-cached rbtree API without the cache:

  • rbtree.h:100-101 rb_insert_color_cached(node, root, leftmost) β€” macro ignores the leftmost argument and only runs INSERT_COLOR; it never writes root->rb_leftmost.
  • rbtree.h:102-103 rb_erase_cached(node, root) β€” only RB_REMOVE; never updates root->rb_leftmost.
  • rbtree.h:104 rb_first_cached = RB_MIN over the real tree (correct).
  • rbtree.h:175 LINUX_RB_ROOT_CACHED initializes rb_leftmost = NULL.

Consequently rb_root_cached.rb_leftmost is NULL for the lifetime of the tree. amdgpu_vm_fini() (amdgpu_vm.c:3316-3334) replaced the upstream postorder cleanup with:

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

The loop body never executes: any amdgpu_bo_va_mapping still in the interval tree at VM teardown is silently leaked (its 72-byte allocation plus rb node), and the dev_err("still active bo inside vm") case (amdgpu_vm.c:3313-3315) degenerates to leak-without-cleanup. This is a DragonFly-specific divergence (upstream uses rbtree_postorder_for_each_entry_safe, which works).

Normally the tree is empty at fini because the GEM close path removes all mappings; the leak manifests for mappings added by kernel-internal callers or any close-ordering that leaves stray mappings (KFD paths).

Fix

fix.diff swaps the dead loop for an rb_first()-based walk (RB_MIN works under the shim). The shim macros should also grow real leftmost maintenance for other future users β€” the same broken pattern is latent for anyone else using rb_leftmost directly.

VERDICT.md
↓ download raw

DF-3865 VERDICT β€” not_reproduced (hardware-gated); shim defect certain at source

Runtime

No AMD GPU on the audit guest β†’ no VM is ever created β†’ fini path never runs. status = not_reproduced, reproduced = 0. (The defect is provable purely from the shim macros; no execution needed.)

Source proof

  1. sys/dev/drm/include/linux/rbtree.h:100-101:
#define rb_insert_color_cached(node, root, leftmost)            \
    linux_root_RB_INSERT_COLOR((struct linux_root *)(&(root)->rb_root), (node))

The leftmost parameter is discarded; no write to root->rb_leftmost anywhere in the macro.

  1. rbtree.h:102-103: rb_erase_cached likewise only removes.

  2. rbtree.h:175: #define LINUX_RB_ROOT_CACHED (struct rb_root_cached) { {NULL, }, NULL } β†’ rb_leftmost starts NULL and stays NULL forever.

  3. amdgpu_vm.c:3055 vm->va = LINUX_RB_ROOT_CACHED; β†’ NULL leftmost. amdgpu_vm.c:3316-3334 (the DragonFly branch) loops while (vm->va.rb_leftmost) β€” dead; the kfree at 3333 is unreachable. Upstream v4.20 uses rbtree_postorder_for_each_entry_safe here (verified) β€” the DFly replacement is the divergence.

  4. Consequence: every mapping still present in vm->va at fini leaks (kfree skipped). Affected scenario: fini with non-empty tree (amdgpu_vm.c:3313 even prints "still active bo inside vm" for it).

Severity Low (bounded leak, rare path, no memory-safety violation), but a real, certain shim-contract bug β€” and a landmine for any other code that trusts rb_leftmost under this shim.

Fix validation

fix.diff replaces the dead loop with an rb_first()-based loop (rbtree.h:97 rb_first = RB_MIN β€” functional under the shim); git apply --check clean. Not exercisable on this guest: fix_status = not_testable.

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.diff replaces the loop with an rb_first()-based walk; git apply --check clean; not exercisable without AMD hardware

findings/poc/DF-3865/fix.diff
↓ fix.diffper-fix-DF-3865

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (macro-by-macro proof + fini loop); fix.diff swaps the loop for an rb_first()-based walk (rb_first=RB_MIN works under the shim)

PoC changes

authored from scratch (static-analysis finding; no runtime trigger possible)

Verified recommended fix

Replace the dead loop with while ((mapping = rb_entry_safe(rb_first(&vm->va.rb_root), struct amdgpu_bo_va_mapping, rb))) (fix.diff); separately fix the shim macros to maintain rb_leftmost

Verdict

Source-certain shim-contract bug, DFly-specific: rb_insert_color_cached (rbtree.h:100-101) discards its leftmost argument and rb_erase_cached (:102-103) never touches root->rb_leftmost, so rb_root_cached.rb_leftmost is NULL for the tree's lifetime (initializer rbtree.h:175). amdgpu_vm_fini's DFly-specific cleanup loop 'while (vm->va.rb_leftmost)' (amdgpu_vm.c:3325) therefore never executes - upstream's rbtree_postorder_for_each_entry_safe (which the #ifndef branch still uses) is functionally replaced by a no-op: every amdgpu_bo_va_mapping still in the tree at VM teardown leaks (kfree at :3333 unreachable), incl. the dev_err('still active bo inside vm') case at :3313. Leak-only (Low), normally masked because GEM close removes mappings first; kernel-internal/KFD-ordered teardowns can hit it. Guest has no AMD GPU: not reproducible at runtime; defect proven from the macros alone.