# DF-5017 VERDICT — dma_fence_wait_any_timeout: wrap + truncation + lost wakeup

## Status: REPRODUCED (kernel-logic/DoS class; no memory corruption)

## Root cause

`dma_fence_wait_any_timeout()` — sys/dev/drm/linux_fence.c:171-222 — ports
the Linux any-fence wait to the DragonFly sleep primitives but drops every
safety property of the original wait_event/schedule_timeout idiom:

```c
206:	end = jiffies + timeout;
207:	for (ret = timeout; ret > 0; ret = MAX(0, end - jiffies)) {
208:		if (dma_fence_test_signaled_any(fences, count, idx))
209:			break;
210:		err = tsleep(current, intr ? PCATCH : 0, "dfwat", ret);
```

* **(1) wrap** — line 206/207: unsigned `end - jiffies` underflows at
  deadline; `MAX(0, ULONG_MAX)` stored in `long ret` is negative; returned
  to callers as an errno. Contract (Linux dma-fence API): 0 = timeout,
  negative = real error. Upstream returns remaining jiffies via
  `schedule_timeout()` which never goes negative.
* **(2) truncation** — line 210: `tsleep(..., int timo)` receives a `long`.
  `(int)MAX_SCHEDULE_TIMEOUT == -1`; `sys/kern/kern_timeout.c:968-970`
  (`_callout_setup_quick`) clamps `qtick - softticks <= 0` to `softticks+1`,
  so each "infinite" iteration wakes on the next tick: the infinite wait is
  a ~hz-rate poll loop for its whole duration (TEST E: 52 wakeups/s).
* **(3) lost wakeup** — lines 208→210: the signaled-check runs without any
  lock and the sleep is not interlocked beforehand; `dma_fence_signal()`
  (linux_fence.c:241-244 walk under `fence->lock`) can call
  `wake_up_process(current)` (wakeup on the task ident) *after* the check
  but *before* `tsleep()` queues the thread (`_tsleep_interlock`,
  sys/kern/kern_synch.c:575-577). DragonFly wakeup is not latched for
  not-yet-queued idents → the completion notification is lost and the
  waiter sleeps the full remaining timeout. `dma_fence_default_wait` does
  not share this flaw because `lksleep` releases/acquires `fence->lock`
  atomically with the sleep (linux_fence.c:100/137).

## Reproduction (guest, fresh `vm.sh reset with-src`, uname in env.txt)

KLD `df5017.c` calls the *shipped* `dma_fence_wait_any_timeout` from
drm.ko and a byte-equivalent local copy with the fix, in one run:

* TEST A (400 unsignaled waits, timeout=1 tick): baseline 399/400 returned
  **negative** on a clean timeout (one 0); fixed 400/400 returned 0.
  (`run.2.log` — deterministic, 399/400 both sessions.)
* TEST B (MAX_SCHEDULE_TIMEOUT, intr=false, signaled at 3 s): baseline
  returned `ret=9223372036854775507` after 300 ticks — i.e. the loop only
  terminated because of the tick-rate polling (defect 2), not because an
  indefinite sleep worked; fixed returned `ret=1` after 300 ticks.
* TEST E (truncation primitive): `tsleep(id, 0, "x", (int)LONG_MAX)` woke
  52 times in 1 s — an infinite sleep wakes 0 times.
* TEST C (lost-wake primitive, first session `run.log`): waiter "checked"
  (flag) then tsleep(1 s); waker on another CPU fired `wakeup()` within ns
  of the flag: trial 4 slept the **full 100 ticks** (wake lost); the 9
  other immediate trials were delivered (the window is narrow — exactly the
  point: it is a race, not a certainty); all 200 ms-control trials slept
  19-20 ticks (sanity: the mechanism works when the wake follows the
  sleep).

Guest stayed up throughout the recorded run; module loads and unloads
cleanly. (One earlier session ended in a guest wedge caused by my own TEST C
harness kthread lifecycle — a harness artifact, retired from the automated
flow; its fence results are the ones cited above. One session before that
panicked on a NULL kthread arg — again a harness bug, fixed.)

## Impact

No memory corruption. Wrong synchronization semantics on the DRM fence API:

* unprivileged render-node users get spurious `-errno` from
  `DRM_IOCTL_AMDGPU_WAIT_FENCES` on legitimate timeouts (amdgpu_cs.c:1560;
  `amdgpu_gem_timeout` maps negative `timeout_ns` to MAX_SCHEDULE_TIMEOUT —
  amdgpu_gem.c:414-415);
* "infinite" waits (amdgpu_sa.c:322, intr=false) become 100 Hz poll loops —
  cheap per tick but per-thread, unbounded, and unkillable for the duration
  (PCATCH unset on that caller);
* rare lost completions stall a waiter the full user-supplied timeout and
  misreport a signaled fence as timed out.

On the standard audit guest there is no GPU, so the in-tree ioctl paths were
not exercisable; the defects were demonstrated directly against the shipped
drm.ko implementation (deterministic for defects 1-2, single-hit race for
defect 3).

## Fix

`fix.diff` — rewrite the wait loop with the shim's
`set_current_state()/schedule_timeout()` idiom (its kt_spin + TASK_RUNNING
latch closes the race; `timo` conversion handles MAX_SCHEDULE_TIMEOUT; the
return is non-negative). Validated two ways: the identical algorithm
compiled into the PoC module (fixed columns in run.2.log) and the patched
`linux_fence.c` compiles clean as a kernel module with full -Werror flags
(fix_build.log).

Fix status: **fixed** (behavioral A/B in-run + TU compile check;
full-kernel rebuild not performed — not a memory-corruption class finding).

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