# DF-5138 — VERDICT

**status: reproduced (mechanism, runtime-proven on the audit guest)**
**reproduced: 1 (mechanism) · impact: none on this guest (ceiling: local DoS on GPU hardware)**

## The finding

`ttm_eu_reserve_buffers()` (`sys/dev/drm/ttm/ttm_execbuf_util.c:93-178`)
contains a duplicate-BO separation mechanism that is **dead code on
DragonFly**:

```c
ret = __ttm_bo_reserve(bo, intr, (ticket == NULL), ticket);   // :113
...
} else if (ret == -EALREADY && dups) {                        // :119
        struct ttm_validate_buffer *safe = entry;
        entry = list_prev_entry(entry, head);
        list_del(&safe->head);
        list_add(&safe->head, dups);
        continue;
}
```

It depends on the Linux ww_mutex contract: locking a ww_mutex already held
by the SAME acquire context returns `-EALREADY`. DragonFly's ww_mutex
(`sys/dev/drm/linux_wwmutex.c`, DFly-original) never does:

- `ww_mutex_init()` initializes the lockmgr with **`LK_CANRECURSE`**
  (linux_wwmutex.c:62);
- lockmgr's same-thread exclusive relock **silently succeeds**
  (sys/kern/kern_lock.c:125-143: "If the caller already holds the lock
  exclusively then we silently obtain another count");
- so `__wwlock()` returns 0 and executes `ww->ctx = ctx; ww->stamp =
  ctx->stamp; ++ctx->acquired;` a second time (linux_wwmutex.c:117-120).
- grep confirms: no `-EALREADY` producer exists anywhere in
  linux_wwmutex.c / ww_mutex.h.

Consequences, traced and (for the core mechanism) runtime-proven:

1. **Duplicate BOs are recursively double-locked.** Both consumers can
   deliver duplicates from unprivileged userspace: the radeon CS relocs
   chunk looks up each handle independently (radeon_cs.c:103-104 — same
   handle twice → two entries), and `amdgpu_bo_list_create()` does not
   dedupe either (amdgpu_bo_list.c: lookup per info[i].bo_handle). On
   Linux the second entry goes to `dups`; on DFly both stay in `list` and
   the resv is locked twice.
2. **Ticket accounting desync.** `ww_mutex_unlock()`
   (linux_wwmutex.c:188-202) decrements `ctx->acquired` and NULLs
   `ww->ctx` only on the FIRST unlock of a resv; the second unlock of the
   recursive hold finds `ww->ctx == NULL` and skips the decrement →
   `ctx->acquired` over-counts. Measured: acquired=1 after balanced
   lock/unlock (run.log). Effect: `__wwlock()` later takes the
   LK_NOWAIT+tsleep route (linux_wwmutex.c:110-114) where a plain blocking
   lock was correct — behavioral skew only (liveness analyzed sound).
3. **Failure-path wedge (conditional).** If the uh-oh path fires between
   the two occurrences (e.g. -EDEADLK), the reverse-backoff unlocks the
   first episode, the loop re-acquires, and lock counts re-balance
   (traced); but if `reservation_object_reserve_shared()` fails
   (krealloc -ENOMEM, linux_reservation.c:93-94) on the SECOND occurrence,
   the reverse-backoff releases the first episode and the second recursive
   episode is never unlocked (`ttm_eu_backoff_reservation_reverse` is
   exclusive of the current entry — list.h:187-189) → the BO's reservation
   lock is permanently held. This is the DF-2136 leak made reachable via a
   user-controlled duplicate list entry (on Linux the dup would have been
   moved to `dups` and never double-locked). Every later CS touching that
   BO blocks in `ww_mutex_lock_slow` (killable when intr, unkillable for
   the `intr=false` callers, e.g. amdgpu_gem.c:177).
4. **Fence side is refcount-safe** (traced, killed as a corruption
   candidate): `reservation_object_add_shared_fence` /
   `add_excl_fence` take their own refs (linux_reservation.c:109/151/244);
   a shared fence with the same context replaces in place
   (linux_reservation.c:120-127). Double fence installation on a duplicated
   resv is semantically odd but refcount-balanced, and the lockmgr count
   balances (two LK_RELEASE for two acquisitions).

A second consumer already expects the contract: `modeset_lock()`
(drm_modeset_lock.c:270-276) maps `-EALREADY` to "we already hold the lock..
this is fine" — dead code on DFly today for the same reason.

## Runtime proof (guest, stock kernel)

`df5138_wwproof.ko` performs the exact `ttm_eu_reserve_buffers` sequence for
a duplicate BO (two `ww_mutex_lock(&ww, &ctx)`, then the unlocks
`ttm_eu_fence_buffer_objects()` would perform — one per entry still in the
list). Stock run (run.log, run.2.log — 3 identical runs total):

```
DF5138-PROOF: first_lock=0 second_lock=0 (EALREADY would be -37)
DF5138-PROOF: ticket->acquired after both locks = 2 (...)
DF5138-PROOF: after unlocks ticket->acquired = 1 (correct value: 0; 1 proves the desync)
DF5138-PROOF: resv still locked? 0 (...)
DF5138-PROOF: RESULT=REPRODUCED (no -EALREADY; silent recursive double-lock; ticket accounting desync)
```

`ww_mutex_is_locked()==0` also confirms the success path is lock-balanced
(the bug is NOT a straight memory-corruption primitive — the dangerous
manifestations are the failure-path wedges in 2/3 above, hardware-gated
behind a GPU CS ioctl this guest lacks).

## Fix validation (performed in the same run)

`fix.diff` restores the contract in `__wwlock()`:

```c
if (ctx != NULL && ww->ctx == ctx)
        return (-EALREADY);
```

Safety: `ctx` is private to the calling thread; observing `ww->ctx == ctx`
implies this thread holds the lock (own store visible to own load; no other
thread can store our ctx pointer). Applied inside the guest `/usr/src`,
drm.ko rebuilt (fix_build.log, 105 compile units, clean -Werror), installed,
guest rebooted (see unload_hang.txt — stock `kldunload drm` hang forced a
reboot; environment issue, unrelated), proof re-run (fix_run.log):

```
DF5138-PROOF: first_lock=0 second_lock=-37 (EALREADY would be -37)
DF5138-PROOF: ticket->acquired after both locks = 1 (...)
DF5138-PROOF: after unlocks ticket->acquired = 0 (correct value: 0; ...)
DF5138-PROOF: resv still locked? 0 (...)
DF5138-PROOF: RESULT=FIXED (-EALREADY returned; dup separated; accounting correct)
```

With the fix, `ttm_eu_reserve_buffers`' dups branch is live, the duplicate
is separated, and both the modeset `-EALREADY` handler and the ttm dups
handling behave as upstream intends. Baseline REPRODUCED / patched FIXED —
bad behavior gone.

## Why severity is Low

- The mechanism is certain and runtime-proven, but the memory-safety-adjacent
  consequences (3) need (a) radeon/amdgpu GPU hardware (absent on the audit
  guest), (b) a duplicate handle in a CS list, and (c) an allocation failure
  at the right instant — narrow trigger, local DoS ceiling, overlapping
  DF-2136's leak family for the wedge symptom.
- No information disclosure, no corruption on the success path.

## Exploit chain

(conditional DoS only: CS ioctl with duplicate BO handle + reserve_shared
-EINTR/-ENOMEM timing → permanently-held resv lock → all later users of that
BO block; no escalation path — the desync never produces attacker-controlled
memory writes.)

## Environment note

`kldunload drm` of the STOCK module hangs on this guest (D-state, holds the
kld lock; unload_hang.txt) — pre-existing, unrelated to the finding/fix.
Guest left clean via `vm.sh reset with-src`.

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