# 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`
