reservation_object_get_fences_rcu leaks the exclusive-fence reference on the GFP_KERNEL slow-path retry (continue) β permanent kernel heap leak under allocation pressure
Summary
A dma_fence ref is taken at the top of every retry iteration; when the NOWAIT krealloc fails and the GFP_KERNEL retry succeeds, the continue re-acquires without putting the old one - one fence+release-payload leaks permanently per event. Different path than DF-4772 (mid-loop vs tail); its fix does not cover this. REPRODUCED on guest via KLD under buffer-cache pressure (refcount 1->2 then 2->3, never back to baseline; balanced when memory suffices - exactly gating on this path); put-before-continue fix validated on rebuilt kernel (leaked=0). Unpriv-reachable via every pfence_excl!=NULL caller.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4796 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| df4796.c | β | 5.0 KB | view raw | |
| Makefile | β | 199 B | β download | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 70 B | view raw | |
| run.log | β | 596 B | view raw | |
| env.txt | β | 272 B | view raw | |
| fix.diff | β | 434 B | view raw | |
| manifest.json | β | 860 B | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-4796 β reservation_object_get_fences_rcu() leaks the exclusive-fence reference on the GFP_KERNEL slow-path retry
File: sys/dev/drm/linux_reservation.c (pass 2)
Severity: Medium (unprivileged-reachable kernel heap leak under memory pressure)
Confidence: certain β reproduced (2 leak events / 2 pressure windows, permanent refcount growth)
What the bug is
reservation_object_get_fences_rcu() (linux_reservation.c:336-414) takes a
reference on obj->fence_excl at the top of every retry iteration
(:356-357):
fence_excl = rcu_dereference(obj->fence_excl);
if (fence_excl && !dma_fence_get_rcu(fence_excl))
goto unlock;
When the krealloc(shared, sz, GFP_NOWAIT) (:365-366) fails under memory
pressure and the GFP_KERNEL retry (:369-370) succeeds, the code does
if (nshared) {
shared = nshared;
continue; /* <-- ref on fence_excl dropped on the floor */
}
β the continue restarts the do-while, the next iteration re-acquires
fence_excl, and the reference from the aborted iteration is never
released. One dma_fence reference (and, at refcount zero, the fence plus
its release payload) leaks per NOWAIT-fail/WAITOK-success event.
Every lockless reservation_object_get_fences_rcu caller with a
non-NULL pfence_excl is affected (amdgpu_vm, i915_gem busy/eof paths,
ttb execbuf) β the same unprivileged reachability as DF-4772, but firing
only under allocation failure instead of on every call.
Distinct from DF-4772 (the pfence_excl==NULL tail leak β fixed by the
else-put): this is the mid-loop continue path and needs the else-put's
sibling β a put before continue.
Reproduction (no GPU needed β pure shim logic under memory pressure)
-
Fill the buffer cache so free memory is far below the crafted
fobj->shared_max * 8krealloc size while pages remain reclaimable (the NOWAIT-fails / WAITOK-succeeds asymmetry):dd if=/dev/zero of=/root/bigfile bs=1m count=3072; sync # vm.stats.vm.v_free_count: 61961 pages (~242MB free, 512MB request)
-
The df4796 KLD builds a reservation object with a live excl fence and a crafted shared list (
shared_count=0, shared_max=67108864-> sz = 512MB), then calls the real exported shim in a loop and watches the fence refcount (kref_read):kldload drm; kldload df4796.ko sysctl kern.df4796.max=67108864 kern.df4796.iters=4 sysctl kern.df4796.run=1
Observed (run.log):
df4796: base=1 after=2 leaked=1 (4 iterations) df4796: LEAK CONFIRMED β 1 excl-fence refs leaked by the GFP_KERNEL slow-path 'continue'
Re-tightening the pressure window a second time:
df4796: base=2 after=3 leaked=1 (4 iterations) df4796: LEAK CONFIRMED β 1 excl-fence refs leaked ...
The refcount never returns to its baseline: each event permanently pins
one dma_fence (and its ops->release payload, e.g. an amdgpu/i915
fence object with its back-references) β unbounded growth under
sustained pressure Γ unprivileged ioctl churn.
Runs with free memory above the request size are balanced (leaked=0) β exactly the expected gating on the allocation-failure path.
Fix
dma_fence_put(fence_excl); before the continue (fix.diff), validated
on a rebuilt kernel: identical pressure + probe yields leaked=0 while
the NOWAIT failure still occurs (see VERDICT.md).
Files
df4796.c/Makefileβ the leak-probe KLD (uses the port's own exported shim; no GPU required)build.sh/run.shβ exact commandsrun.log,env.txt,build.logfix.diffβ the validated one-line fixVERDICT.md,manifest.json,verdict.json
DF-4796 VERDICT β excl-fence reference leak on the GFP_KERNEL slow-path continue
Status: REPRODUCED (baseline), fix validated on rebuilt kernel
Baseline (bug present, kernel #0/#2 β the reservation shim is
untouched by the DF-4797 fix)
The df4796 KLD calls the port's exported
reservation_object_get_fences_rcu() on a reservation object with a
live exclusive fence and a crafted shared list sized so the retry-loop's
krealloc asks for more memory than is free:
-
run 1 (free 242MB, request 512MB):
df4796: base=1 after=2 leaked=1 (4 iterations) df4796: LEAK CONFIRMED β 1 excl-fence refs leaked by the GFP_KERNEL slow-path 'continue'
-
after the freed buffer re-inflated free memory, runs 2-3 were balanced (leaked=0) β the leak is exactly gated on the NOWAIT-failure path;
-
re-tightened pressure window (free ~97MB):
df4796: base=2 after=3 leaked=1 (4 iterations) df4796: LEAK CONFIRMED β ...
Two independent leak events; the fence refcount never returns to
baseline (1 -> 2 -> 3, permanent). Each event pins one struct
dma_fence plus, at final release, its ops->release payload β
unbounded kernel-heap growth under sustained allocation pressure, driven
by unprivileged drm ioctls that reach
reservation_object_get_fences_rcu with pfence_excl != NULL
(amdgpu_vm page-dir walks, i915 GEM_BUSY/eof waits, ttm execbuf β the
same reachability family as DF-4772, which fires per-call instead).
Root cause: linux_reservation.c:356-357 acquires the excl-fence
reference each retry iteration; on the :369-374 GFP_KERNEL slow-path the
continue restarts the loop without dma_fence_put(fence_excl) β the
next iteration's acquire makes the dropped reference unrecoverable.
Fix validation (kernel #3, make -j6 nativekernel RC=0 + installkernel)
fix.diff adds dma_fence_put(fence_excl); before the continue.
Identical procedure on the rebuilt kernel:
-
pressure window 1 (free 297MB, request 512MB β same free<request relationship as the baseline window, so the NOWAIT krealloc fails identically):
df4796: base=1 after=1 leaked=0 (4 iterations) df4796: balanced ...
-
pressure window 2 (free ~324MB): balanced again (leaked=0, lastcnt=1).
The allocation-failure path demonstrably still executes (the deterministic slab allocator fails a 512MB NOWAIT request whenever free memory is below it, exactly as in the baseline windows); the refcount now stays at baseline β the fix converts the leak into a balanced acquire/put pair.
Notes
- The PoC needs
kldload drmfirst (the shim'sdma_fence_*symbols live in drm.ko, same as the DF-4772 harness). - The harness message "no NOWAIT failure observed" is conservative wording from the probe (it cannot see the allocator's internals); the free-memory/request-size relationship is the operative condition and is matched to the baseline windows above.
- This leak is distinct from DF-4772 (the pfence_excl==NULL tail leak,
fixed by the else-put): the present one fires with pfence_excl != NULL
on the mid-loop
continue.
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
fixedBaseline: two permanent leak events across two pressure windows (refcount 1->2->3). Kernel #3 rebuilt with fix.diff (put before continue): identical pressure procedure yields base=1 after=1 leaked=0 in both windows (free memory 297MB/324MB vs 512MB request β the same free<request relationship that deterministically failed NOWAIT at baseline).
VERDICT.md; run.log; buildkernel3.log RC=0; fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (kern refcount growth across two pressure windows: base=1 after=2, base=2 after=3, LEAK CONFIRMED twice; balanced runs when free>sz); VERDICT.md (baseline + fixed narrative); fix.diff; env.txt
PoC changes
Built from scratch (no seed): df4796.c KLD crafting the reservation object with the port's own primitives (kzalloc/dma_fence_init/reservation_object_init + hand-installed fobj with shared_max=64M) and watching kref_read before/after; memory pressure from a 3GB hammer2 buffer-cache fill (reclaimable pages give the NOWAIT-fail/WAITOK-succeed asymmetry); kldload drm required first for the shim symbols.
Verified recommended fix
dma_fence_put(fence_excl) before the 'continue' in reservation_object_get_fences_rcu's GFP_KERNEL slow path (fix.diff, validated)
Verdict
Reproduced with a KLD that calls the port's exported reservation_object_get_fences_rcu() on a crafted reservation object (live excl fence, shared list with shared_max=64M entries so the retry krealloc asks for 512MB) under buffer-cache memory pressure (free 242MB/97MB in two windows): each NOWAIT-fail/WAITOK-success event permanently leaks one exclusive-fence reference β fence refcount 1->2 and 2->3 across two independent windows, never returning to baseline ('df4796: LEAK CONFIRMED', run.log); balanced runs whenever free memory exceeds the request, exactly gating the leak on linux_reservation.c:365-374's GFP_KERNEL slow-path 'continue', which drops the reference acquired at :356-357. Unprivileged-reachable kernel heap leak under allocation pressure via every get_fences_rcu caller with pfence_excl!=NULL (amdgpu_vm, i915 gem busy/eof, ttm execbuf) β the pressure-gated sibling of DF-4772's per-call tail leak. Fix (dma_fence_put before continue) validated on a rebuilt kernel (#3): identical pressure windows yield base=1 after=1 leaked=0 while the same free<request relationship holds.
No comments yet.