# DF-5182 — Verification Verdict

**Status:** reproduced
**Impact:** dos (spurious-timeout misreport + stalled wait; per-call, bounded
by the caller's timeout; poll variants self-heal ≤1 s late)
**Confidence:** certain (mechanism deterministically instantiated against the
unmodified in-tree `linux_wait.c`; real-world entry probability reasoned
below)

## The bug

`__wait_event_common()` (sys/dev/drm/include/linux/wait.h:110-169) checks
the condition **before** interlocking the sleeper:

```c
while (1) {
        __wait_event_prefix(&wq, flags);            /* wait.h:125 */
        if (condition)                              /* wait.h:127 */
                break;
        tsleep_interlock(current, flags);           /* wait.h:130 */
        ret = tsleep(current, PINTERLOCKED|flags, "lwe", timeout); /* :133 */
```

A `wake_up()` (wait.h:67-74 → `__wake_up_core()` linux_wait.c:49-62 →
`autoremove_wake_function()` linux_wait.c:37-47) whose `wakeup(task)`
executes after the condition evaluation but before `tsleep_interlock()`
finds nobody interlocked and is **dropped**. Worse,
`autoremove_wake_function()` unlinks the wait entry because
`wake_up_process()` (sched.h:229-243) **always returns 1** ("Always
indicate the process was woken up") even when it delivered nothing — so
every subsequent `wake_up()` walks past the stranded sleeper too. The
sleeper burns its whole timeout and `wait_event_timeout()` returns 0
("condition never became true") although the condition was stored true
before the wake. Linux closes this with the task-state interlock
(`prepare_to_wait_event` sets state, and `schedule()` refuses to sleep a
RUNNING task); the DragonFly shim's `tsleep(9)` ignores
`task_struct.state`, so that safety net does not translate — only the
`tsleep_interlock`/`PINTERLOCKED` pair does, and it is ordered one step
too late.

## How it was reproduced

Module `lwqdemo.ko` compiles the pristine `sys/dev/drm/linux_wait.c` and
calls the real macro. The wait condition helper models a condition with
non-trivial width between its state read and its return (as production
drm conditions have — e.g. `radeon_fence_any_seq_signaled()` reads
`RADEON_NUM_RINGS` atomic64 counters, radeon_fence.c:507-511; i915 uses
`wait_event_interruptible_timeout(error->reset_queue, ...)` in
i915_gem.c:120). In "windowed" rounds the waker is admitted into that
slot by a `window_open`/`poke` handshake and performs exactly the
production protocol: `gen++` (condition true) then `wake_up()`.

* **9/9 windowed rounds:** first wake lost (delivered between the state
  read and the interlock), entry auto-removed, sleeper burned the full
  2 s (`dt=199-200` ticks), returned 0 while `gen != g0`.
* **9 rescue wakes** fired ~100 µs into the stranded rounds — none could
  reach the sleeper (entry unlinked). Without the unconditional unlink
  the rescue would have woken it; that contrast is the autoremove half
  of the proof.
* **8/8 control rounds** (bump+wake after the interlock) returned
  immediately with `ret > 0` — the harness plumbing is sound and
  out-of-window wakes work normally.

## Negative result that sharpens the impact claim

A pure timing attack (randomized-offset external waker, 100-cycle sweep
steps over 0-30000 TSC cycles, 6-vCPU KVM guest) completed
**1,666,602 rounds with 0 misreports** (recorded in run.log, first
summary). Reason: the waker's store→`wakeup()` delivery pipeline
(lockmgr + list walk + two spinlocks + sleep-queue scan, ≳500 ns) is
always slower than the waiter's condition→interlock path (≈20
instructions), so a same-cost waker cannot land inside a zero-width
window. The window is entered in production when

1. the condition expression has real width between reading the awaited
   state and returning (all non-trivial drm conditions), or
2. the waiter is preempted/descheduled between check and interlock
   (interrupt, IPI, scheduler — ordinary on a loaded GPU box), which
   widens the window by the handler duration.

Combined with the volume of drm waits (every fence wait, vblank wait,
gmbus transaction, reset wait), lost wakes are an under-load event, not a
laboratory-only one. Consequences: `wait_event_timeout()` callers take
their timeout error path spuriously — e.g. radeon fence waits report
`-ETIME` and can trigger needless GPU-reset escalation
(radeon_fence.c:507-535), i915 `i915_reset_wait`-style users return
`-ERESTARTSYS`/`-EAGAIN` to unprivileged render clients (i915_gem.c:120),
intel_dp gmbus waits fail an AUX transfer (intel_dp.c:1137) — plus the
stall itself for the caller's full timeout.

## Reachability / threat model

The wait primitives are kernel-internal; unprivileged users reach them
through drm ioctls on real GPU hardware (render nodes on some setups,
console user otherwise). The QEMU guest has no drm device, so the PoC
drives the primitives directly from a root-loaded KLD — the
demonstration is of the kernel primitive, while the in-tree call sites
above define the production exposure.

## Fix

Authoritative `fix.diff` in this directory (two parts):

1. reorder the macro so `tsleep_interlock()` precedes the condition
   check (the standard DragonFly interlock idiom — a wake delivered
   after the interlock is caught by the sleep-queue interlock or by the
   `TDF_TSLEEPQ` re-validation inside `tsleep()` itself,
   kern_synch.c:668-676);
2. make `wake_up_process()` report whether a wakeup was actually
   delivered (`sched.h`), so `autoremove_wake_function()` only unlinks
   entries that were really woken (linux_wait.c:43-44).

Not validated by a kernel rebuild (Medium logic/race finding; the diff is
kept minimal and mechanical).

## Artifacts

* `lwqdemo.c` — final harness source (sha256
  1978c8c3c8887476ea99e9125e41040d68b2484aa4895ca87ab0ad09d9850632)
* `Makefile`, `build.sh`, `run.sh`
* `build.log` — full `-Werror` build of the module incl. pristine
  linux_wait.c (rc=0)
* `run.log` — full dmesg: statistical run (1.67 M rounds, 0) AND
  deterministic run (9 misreports, 8 controls, 9 rescues, SUMMARY)
* `env.txt` — guest uname/compiler/vcpus/TSC
* `fix.diff`

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