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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4796 Β· 12 files
FileTypeDescriptionSize
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)

  1. Fill the buffer cache so free memory is far below the crafted fobj->shared_max * 8 krealloc 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)
    
  2. 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 commands
  • run.log, env.txt, build.log
  • fix.diff β€” the validated one-line fix
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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 drm first (the shim's dma_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Wed Sep 23 23:05:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.