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)
PoC verification
Evidence pack
findings/poc/DF-4772 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedA/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)
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_ids.c:119
- sys/dev/drm/amd/amdgpu/amdgpu_ids.c:111-166
- sys/dev/drm/linux_reservation.c:356-357
- sys/dev/drm/linux_reservation.c:406-413
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:985
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1076-1077
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:1559
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:1925
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:1330
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).
No comments yet.