# DF-5017 — `dma_fence_wait_any_timeout`: unsigned deadline wrap + long→int sleep-timeout truncation + lost-wakeup race

## What

Three defects in one loop — `sys/dev/drm/linux_fence.c:206-215` — the
any-fence wait used by the amdgpu stack (and any KLD calling the exported
API):

1. **Unsigned deadline wrap (misreported timeout)** — `end = jiffies + timeout`
   (line 206) with `ret = MAX(0, end - jiffies)` (line 207): once the deadline
   passes, `end - jiffies` underflows to ~ULONG_MAX, `MAX(0, ULONG_MAX)` is
   huge, and storing it into `long ret` yields a **negative** value that the
   loop guard (`ret > 0`) propagates to the caller as a bogus *error* instead
   of the contract's `0` (timeout). Same defect family as DF-2046
   (`dma_fence_default_wait`), duplicated in this different function with
   different callers.
2. **`long → int` truncation of the sleep timeout** — `tsleep(..., ret)`
   (line 210) takes an `int timo`. For `timeout = MAX_SCHEDULE_TIMEOUT`
   (what `amdgpu_sa.c:322` passes, and what `amdgpu_gem_timeout()`
   (amdgpu_gem.c:414-415) returns for user `timeout_ns` with the sign bit
   set), `(int)LONG_MAX == -1`; `_callout_setup_quick`
   (sys/kern/kern_timeout.c:968-970) clamps non-positive tick counts to fire
   on the *next tick*, so the "infinite" wait degenerates into a
   **tick-rate poll loop** (measured: 52 wakeups/s, TEST E) that keeps
   waking the thread ~100×/s until a fence signals — unbounded CPU churn,
   triggerable per-thread by unprivileged GPU clients.
3. **Lost-wakeup race** — unlike `dma_fence_default_wait` (which holds
   `fence->lock` across check+sleep via `lksleep`'s atomic release), the
   wait_any loop checks `dma_fence_test_signaled_any()` (line 208) **without
   any lock** and then calls plain `tsleep()` (line 210). A
   `dma_fence_signal()` on another CPU that fires a callback in the window
   between the check and the tsleep interlock is **dropped** — DragonFly
   `wakeup()` on a not-yet-queued ident is not latched (proven at the
   primitive level: TEST C trial 4, full 1 s sleep despite an immediate
   wakeup; controls delivered at 19/20 ticks). The waiter then sleeps the
   *entire remaining timeout* even though the fence was signaled before it
   slept, and reports `0`/timeout for a fence that was complete.

## PoC (KLD against the live drm.ko symbols — no GPU needed)

```
kldload drm
cd /tmp/df5017 && make
kldload /tmp/df5017/df5017.ko     # runs ~30 s of tests, prints to console
dmesg | grep DF5017
```

The module exercises the *shipped* `dma_fence_wait_any_timeout` (baseline)
and a local copy using the fixed `set_current_state()` + `schedule_timeout()`
idiom (fixed), side by side:

* **TEST A** — 400 unsignaled waits, timeout = 1 tick:
  baseline `ret<0:399 ret==0:1`, fixed `ret<0:0 ret==0:400`.
* **TEST B** — `MAX_SCHEDULE_TIMEOUT`, intr=false, signal after 3 s:
  baseline returns `ret=9223372036854775507` (≈LONG_MAX "remaining") after
  300 ticks — correct-looking only *because of* the tick-rate poll from
  defect 2; fixed returns `ret=1` after 300 ticks.
* **TEST E** — `tsleep(ident, 0, ..., (int)MAX_SCHEDULE_TIMEOUT)`: 52
  wakeups in 1 s (an indefinite sleep would have 0).
* **TEST C** (first guest session, `run.log`) — wakeup fired immediately
  after the waiter's "condition check": trial 4 slept the full 100 ticks
  (WAKEUP LOST); all 200 ms-control trials delivered at 19 ticks.

## Expected (observed, run.2.log)

```
[DF5017] TEST A (400 unsignaled waits, timeout=1 tick)
[DF5017]   baseline wait_any: ret<0:399  ret==0:1  ret>0:0  (contract: every ret must be 0)
[DF5017]   fixed     wait_any: ret<0:0  ret==0:400  ret>0:0
[DF5017]   VERDICT A: BUG — negative return on clean timeout
[DF5017] TEST B baseline: MAX_SCHEDULE_TIMEOUT intr=false, signal@3s -> ret=9223372036854775507 elapsed=300 ticks
[DF5017] TEST B fixed    : MAX_SCHEDULE_TIMEOUT intr=false, signal@3s -> ret=1 elapsed=300 ticks (correct)
[DF5017] TEST E: tsleep timo=(int)MAX_SCHEDULE_TIMEOUT=-1 -> 52 wakeups in 1s ...
```

## Fix

`fix.diff` (applies cleanly to `sys/dev/drm/linux_fence.c`): use the
`sched.h` `schedule_timeout()` shim, whose kt_spin + task-state latch closes
the wakeup race, which clamps the tick count (no truncation), and which
returns non-negative remaining time (no wrap). Validated:
the identical algorithm compiled into the module (fixed columns above), and
the patched translation unit builds clean with full `-Werror` kernel flags
(`fix_build.log`).

## In-tree reachability (real hardware)

* `amdgpu_cs.c:1560` `amdgpu_cs_wait_any_fence` — `DRM_IOCTL_AMDGPU_WAIT_FENCES`,
  unprivileged render-node clients; user-controlled `timeout_ns` (negative →
  MAX_SCHEDULE_TIMEOUT via `amdgpu_gem_timeout`).
* `amdgpu_sa.c:322` `amdgpu_sa_bo_new` — intr=false, MAX_SCHEDULE_TIMEOUT;
  hit by unprivileged submissions whenever the SA manager must wait.
