# DF-2873 VERDICT — REPRODUCED (taskqueue_drain lost-wakeup → permanent hang)

## One-line
`taskqueue_run()` performs `queue->tq_running = NULL; wakeup(task);`
**outside** the queue spinlock (subr_taskqueue.c:411-412), while
`taskqueue_drain()`/`drain_simple()` check the condition under the spinlock
(:493/:512) and then park via `ssleep()` — because the waker never takes
the spinlock, its wakeup can land between the drainer's check and its
sleep-queue interlock enqueue (kern_synch.c _tsleep_interlock, entered from
ssleep while still holding the spinlock) and is lost forever: the drainer
sleeps permanently on ident `task` (wchan "-").

## Contrast with the safe protocol
`taskqueue_thread_enqueue()` (:629-642) is *called with the lock held*
(from taskqueue_enqueue_locked :233-234), so its unlock→wakeup_one is
ordered after any sleeper's interlock — that path cannot lose wakeups.
The :411-412 wakeup has no such ordering.

## Reproduction
- Attempt 1 (2 hammer threads, 90 s): not hit — window is tens of ns.
- Attempt 2 (4 tasks × 4 hammer threads, ~3 min): **HIT**
  `tqlost: LOST WAKEUP REPRODUCED: hammer 3 frozen (h3 15165->15165) with
  ta_pending=0 and task not running` — a drainer thread's iteration counter
  froze for 4+ seconds while its task was provably idle, and `ps -axH`
  shows `lt_h3` permanently asleep on wchan "-" (the drain wmesg).
- A frozen drainer never returns to its caller: on a teardown path this is
  a permanent kernel-thread hang (local DoS, same wedge shape as DF-2870
  but reachable on ordinary busy queues without any free()).

## Fix validation
fix.diff (shared with DF-2869): the runner clears the per-task
TASKQ_RUNNING flag **under** TQ_LOCK and issues `wakeup(task)` only after
unlocking; the sleeper checks the flag under the same lock and interlocks
its sleep with it, so the wakeup can no longer be lost. Bounded negative
re-run on the fixed kernel (same 4×4 hammer, ~3 min): not hit.
See run.fixed.log.
