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

amdgpu_do_test_moves success path leaks all n pinned+reserved+referenced GTT test BOs (~entire GTT) with reservation locks held forever

Summary

Each iteration creates+reserves+pins a 1MiB GTT BO then 'continue's past the error-only unwind - the epilogue releases only vram_obj and the pointer array. n ~= the whole GTT by construction: one successful run permanently exhausts GTT and leaves ~n reservation locks held forever (wedged teardown/unload). Root/boot-gated debug param (amdgpu_testing=1, default 0). Guest-unreachable (no AMD GPU - probe evidence). Guarded post-loop unwind fix, git-apply clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5105 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
probe.sh β€” 734 B view raw
build.sh β€” 94 B view raw
run.sh β€” 153 B view raw
run.log β€” 1.2 KB view raw
env.txt β€” 579 B view raw
fix.diff β€” 566 B view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.3 KB view raw

DF-5105 β€” amdgpu_do_test_moves success path leaks every GTT test BO (pinned + reserved + referenced)

What runs it

amdgpu_test.c is compiled into the stock amdgpu.ko (Makefile:63) and amdgpu_test_moves() is called from amdgpu_device_init() (amdgpu_device.c:2684-2686) iff the load-time module parameter amdgpu.amdgpu_testing & 1 is set (amdgpu_drv.c:85 default 0, amdgpu_drv.c:192 mode 0444, i.e. root/boot-gated) and the GPU accel path is up. No userspace trigger; root opts in at boot/module-load.

Build

Nothing to compile β€” this is a kernel-path finding. The probe script talks to the running kernel:

./build.sh      # no-op (copies probe.sh, no compilation)

Run (inside the guest, as root)

sh /root/df5105/probe.sh        # or ./run.sh via vm.sh

Expected on an AMD-GPU-equipped host with amdgpu.testing=1

On success the loop at amdgpu_test.c:85-234 continues (line 217) past the error-only unwind labels (219-233); after the loop only vram_obj is unpinned/unreserved/unreffed (236-240) and the pointer array is kfree'd (242). The n created GTT BOs (each 1 MiB, pinned, reservation held, refcount 1) are never released β€” n β‰ˆ (gart_size βˆ’ overhead)/1MiB, i.e. essentially the whole GTT. Second-order: their amdgpu_bo_reserve() locks are held forever, so any later eviction/teardown path that tries to reserve them blocks.

What this guest shows

This QEMU guest has no AMD display adapter (only the emulated bochs VGA chip 0x11111234), so amdgpu_device_init() never runs and the test path is unreachable here regardless of the parameter. The probe documents exactly that: module loads, no device attaches. The leak itself is proven by direct reading of sys/dev/drm/amd/amdgpu/amdgpu_test.c:85-242 (see VERDICT.md).

Artifacts

  • probe.sh β€” the guest probe
  • run.log β€” full untrimmed probe output
  • env.txt β€” guest environment
  • VERDICT.md β€” full narrative with line citations
  • fix.diff β€” post-loop unwind of gtt_obj[] (both paths safe)
  • verdict.json / manifest.json
VERDICT.md
↓ download raw

VERDICT β€” DF-5105

Finding (re-derived, line-accurate)

amdgpu_do_test_moves() (sys/dev/drm/amd/amdgpu/amdgpu_test.c:32-246) creates n 1 MiB GTT buffer objects inside its main loop:

  • amdgpu_test.c:92 amdgpu_bo_create(adev, &bp, gtt_obj + i) β€” refcount 1, only pointer is gtt_obj[i]
  • amdgpu_test.c:98 amdgpu_bo_reserve(gtt_obj[i], false) β€” reservation lock ACQUIRED and held
  • amdgpu_test.c:101 amdgpu_bo_pin(gtt_obj[i], AMDGPU_GEM_DOMAIN_GTT) β€” GTT space pinned

On a fully successful iteration the code prints the success line and continue;s:

  • amdgpu_test.c:215-217 DRM_INFO(...); continue;

The unwind labels out_lclean_unpin/_unres/_unref/out_lclean (amdgpu_test.c:219-233) are reachable only by falling into them from the error branches above them (copy failure, kmap failure, data mismatch…); they end in break. A successful iteration never executes them.

After the loop finishes normally, the epilogue releases ONLY the VRAM object and the pointer array:

  • amdgpu_test.c:236 amdgpu_bo_unpin(vram_obj);
  • amdgpu_test.c:238 amdgpu_bo_unreserve(vram_obj);
  • amdgpu_test.c:240 amdgpu_bo_unref(&vram_obj);
  • amdgpu_test.c:242 kfree(gtt_obj);

The kfree(gtt_obj) frees the array of pointers, not the BOs. Every gtt_obj[i] still has refcount 1 (held by the now-freed array slot), is still PINNED, and β€” worse β€” its amdgpu_bo_reserve() from line 98 was never undone, so its reservation lock is held forever by a thread that has returned.

Consequence

n is computed at amdgpu_test.c:47-55 as (gart_size βˆ’ IB pool βˆ’ ring sizes βˆ’ wb βˆ’ IH)/1 MiB β€” i.e. by construction the test allocates essentially the entire GTT. One successful run therefore:

  1. permanently leaks ~n MiB of pinned GTT address space (resource exhaustion: subsequent GTT placements for any amdgpu user fail),
  2. leaves ~n ww_mutexes locked forever β€” later eviction, shrinker, or teardown paths that try to amdgpu_bo_reserve() any of these BOs block indefinitely (potential hang of the drm subsystem / unload).

Reachability

Verification on the audit guest (2026-09-27)

The QEMU/KVM guest has no AMD display adapter (pciconf shows only the emulated bochs VGA, chip 0x11111234; no vendor-0x1002 device). amdgpu.ko loads (kldload rc=0) and prints "[drm] amdgpu kernel modesetting enabled.", but no device attaches, so amdgpu_device_init() never runs and amdgpu_test_moves() cannot be executed here β€” with or without amdgpu_testing=1.

Classification: not_reproduced (target hardware absent). The finding itself is certain from direct source reading β€” the leak needs no runtime condition beyond "the loop completes successfully", which is the test's designed-for success path. No memory-corruption primitive is involved (objects are leaked, not freed-then-used), so there is no escalation chain: the ceiling on AMD-equipped hosts is GTT exhaustion + permanently-held reservation locks β†’ graphics-stack DoS / wedged unload, self-inflicted by the root-administered test parameter.

Fix

fix.diff adds a post-loop unwind of gtt_obj[]. It is safe on both exits: the error path has already amdgpu_bo_unref()ed (and NULLed) every entry it touched, amdgpu_bo_create failure leaves the slot NULL from kcalloc, and the guard if (gtt_obj[i]) makes the loop a no-op there; on success it releases all n BOs in reverse order before kfree(gtt_obj).

Fix validation: not_testable β€” the single-fix kernel build/re-run protocol is meaningless on a guest with no AMD GPU (the path cannot be exercised in either the baseline or the patched kernel).

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

not_testable: single-fix kernel build + PoC re-run is meaningless on a guest with no AMD GPU β€” the code path cannot be exercised in either the baseline or the patched kernel; the fix is validated structurally (guard makes it a no-op on the error path where slots are already unreffed/NULLed by amdgpu_bo_unref, and handles n==0 via i<0)

fix.diff in this pack passes 'git apply --check' against the unmodified sys/ tree; error-path no-op argument documented in VERDICT.md
↓ fix.diffper-fix-DF-5105

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (line-accurate derivation); run.log (probe output: no AMD VGA in pciconf, kldload amdgpu rc=0 with 'kernel modesetting enabled' but zero device-attach lines); amdgpu_test.c:217 'continue' vs 219-233 error-only unwind and 236-242 epilogue freeing only vram_obj + kfree(gtt_obj) of the pointer array; fix.diff applies cleanly (git apply --check OK) and is a no-op on the error path because amdgpu_bo_unref NULLs slots

PoC changes

No seed PoC existed; authored probe.sh fresh (kernel-internal path, so the 'PoC' is a reachability probe: pciconf + kldload + dmesg, all captured untrimmed in run.log)

Verified recommended fix

Add a post-loop guarded unwind of gtt_obj[] (unpin/unreserve/unref for every non-NULL slot) before kfree(gtt_obj) β€” see fix.diff, git-apply-able, no-op on the error path

Verdict

The leak is certain from source: a successful iteration of the move-test loop 'continue's past the error-only unwind labels (amdgpu_test.c:217 vs 219-233), and the epilogue releases only vram_obj plus the pointer array (236-242), so all n GTT BOs β€” each created (92), reserved (98) and pinned (101), refcount held solely by the kfree'd array β€” leak with their reservation locks locked forever; n is by construction (47-55) essentially the whole GTT. Runtime verification was genuinely attempted but the QEMU guest has no AMD display adapter (pciconf shows only the emulated bochs VGA chip 0x11111234): amdgpu.ko loads (kldload rc=0) but no device attaches, so amdgpu_device_init() and thus amdgpu_test_moves() (amdgpu_device.c:2684-2686) never execute here β€” hence not_reproduced/impact=none ON THIS GUEST. Realistic ceiling on AMD-equipped hosts: one root-gated (amdgpu_testing load-time param, amdgpu_drv.c:85/192) successful test run permanently consumes ~all GTT and leaves ~n ww_mutexes held -> graphics-stack DoS and wedged unload. No memory-corruption primitive (objects are leaked, never used-after-free), so no escalation chain exists for this class.