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

amdgpu_pasid_free_delayed leaks one exclusive-fence reference per call β€” the DFly reservation_object_get_fences_rcu shim drops upstream's else-put; unbounded unprivileged kernel heap leak via drm fd open/close churn

Summary

The shim refs resv->fence_excl unconditionally but only hands it out when pfence_excl!=NULL - upstream's else-put was dropped, so every pfence_excl==NULL caller leaks a dma_fence ref. Reachable unpriv per-fd: every amdgpu drm close frees the PASID delayed on the page-directory resv (which carries an excl fence after every update/move) - one leaked fence+release-payload per open/CS/close. REPRODUCED on guest via KLD (1000/1000 and 4096/4096 leaked, linear); else-put fix validated on rebuilt kernel (leaked=0). Same-root call at amdgpu_prime.c:208 covered. No-GPU-needed shim-logic repro.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4772 Β· 12 files
FileTypeDescriptionSize
df4772.c β€” 7.3 KB view raw
Makefile β€” 178 B ↓ download
build.sh β€” 205 B view raw
run.sh β€” 201 B view raw
build.log β€” 1.1 KB view raw
run.log β€” 435 B view raw
run.2.log β€” 467 B view raw
run.fixed.log β€” 359 B view raw
build_kernel.log β€” 977 B view raw
fix.diff β€” 388 B view raw
verdict.json β€” 5.7 KB view raw
env.txt β€” 668 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B on the guest: baseline kernel -> after_get_fences_rcu_NULL=3, released=0 leaked=1000/4096 ('LEAK CONFIRMED'); after applying fix.diff to the guest /usr/src, make -j6 nativekernel RC=0 (under -Werror), installkernel + reboot into #1: Wed Sep 23 13:15:01 UTC 2026, module rebuilt and the identical trigger re-run -> after_get_fences_rcu_NULL=2 (correct), released=1000 leaked=0 ('BALANCED'), guest survived and module unloaded cleanly. /usr/src then restored by vm.sh reset with-src.

run.log + run.2.log (baseline leak) vs run.fixed.log (patched: BALANCED); fix.diff applies clean against sys/ (git apply --check); build_kernel.log (RC=0 tail of the patched nativekernel build)
↓ fix.diffper-fix-DF-4772

Confirmed kernel references

Detail

Exploit chain

unprivileged render-node user -> open /dev/dri/cardD (amdgpu_pasid_alloc) -> submit CS so page tables update (excl fence lands on the PD resv) -> close fd (amdgpu_pasid_free_delayed -> get_fences_rcu(NULL,...) leaks one excl-fence ref) -> repeat; each cycle strands one struct dma_fence (~200B M_DRM + everything its release would have freed) forever -> unbounded kernel heap exhaustion / memory-pressure DoS. No memory corruption: the primitive is a pure reference leak.

Evidence (decisive lines)

run.log (baseline: 'after_get_fences_rcu_NULL=3 (correct value: 2)', 'iterations=1000 released=0 leaked=1000', 'LEAK CONFIRMED'); run.2.log (4096/4096); run.fixed.log (patched kernel: '=2 (correct value: 2)', 'released=1000 leaked=0', 'BALANCED'); build.log (module, -Werror clean); build_kernel.log tail (nativekernel RC=0); fix.diff (git-apply clean vs sys/)

PoC changes

The finding's PoC sketch had to be written from scratch as a KLD harness (no GPU on the guest): kzalloc/kfree (linux/slab.h shim hardwires M_DRM) for the fence containers, lockinit for the fence spinlock (shim has no spin_lock_init), flat df4772. sysctl node (SYSCTL_NODE(,) root, not kern.), per-iteration release bookkeeping via rel_before snapshot (a release_count==i comparison under-counted), and manual kfree of leaked containers at unload so the guest is left clean.

Verified recommended fix

restore upstream's trailing 'else if (fence_excl) dma_fence_put(fence_excl);' in reservation_object_get_fences_rcu (linux_reservation.c:410-411)

Verdict

Reproduced deterministically on the audit guest with a KLD that executes amdgpu_pasid_free_delayed's exact call sequence (amdgpu_ids.c:111-126) against the port's own exported reservation-object shim: reservation_object_get_fences_rcu(resv, NULL, &count, &fences) takes a reference on resv->fence_excl unconditionally (linux_reservation.c:356-357) but only hands it out when pfence_excl != NULL (linux_reservation.c:410-411) β€” upstream Linux's trailing 'else if (fence_excl) dma_fence_put(fence_excl);' is missing from the port. Baseline: fence refcount goes 1 -> 2 (add_excl) -> 3 (get_fences_rcu with NULL) and 1000/1000 then 4096/4096 fence objects are never released (released=0, leaked=N, 'LEAK CONFIRMED'); the leak scales linearly. Real-world reachability is unprivileged: every amdgpu drm fd open allocates a PASID (amdgpu_kms.c:985) and every close calls amdgpu_pasid_free_delayed on the VM page-directory resv (amdgpu_kms.c:1076-1077), which carries an exclusive fence after every page-directory update (amdgpu_vm.c:1559/1925 -> amdgpu_bo_fence(...,true) -> reservation_object_add_excl_fence, amdgpu_object.c:1330) β€” an open/CS/close loop leaks one dma_fence per cycle, unbounded kernel heap growth (kernleak). amdgpu_prime.c:208 has the identical pfence_excl==NULL call (same root cause, separate file). Fix validated on a rebuilt guest kernel: with the else-put restored (fix.diff), the same PoC shows refcount 1->2->2, released=1000, leaked=0, 'BALANCED' (run.fixed.log).