# DF-2873 — taskqueue_drain() lost-wakeup (wakeup outside TQ_LOCK)

## What
`taskqueue_run()` executes, OUTSIDE the queue spinlock
(subr_taskqueue.c:411-412):
```
	queue->tq_running = NULL;
	wakeup(task);
```
while `taskqueue_drain()`/`drain_simple()` check
`task->ta_pending != 0 || task == queue->tq_running` under the spinlock and
then park via `ssleep()`. The waker never takes the spinlock before
`wakeup()`, so its wakeup can land between the drainer's condition check and
its sleep-queue interlock enqueue and be lost forever: the drainer sleeps
for good (wchan "-", wmesg "-") while its task is fully idle.

Contrast with the safe protocol at subr_taskqueue.c:631-642
(taskqueue_thread_enqueue is entered WITH the lock held, so its
unlock→wakeup_one is ordered after the sleeper's interlock).

## Build / Run
see build.sh / run.sh — hammers enqueue+drain on one task from 4 kernel
threads for ~3 minutes; a watchdog reports a hammer whose iteration counter
freezes while `ta_pending == 0` and the task is not running.

## Expected output (stock kernel — reproduced, attempt 2)
```
tqlost: queue 0xfffff80117109348 started (0), hammering drain window
tqlost: LOST WAKEUP REPRODUCED: hammer 3 frozen (h3 15165->15165)
        with ta_pending=0 and task not running
--- ps ---
B2     -        lt_h3      <- permanently parked in taskqueue_drain
```
(Attempt 1, a 2-thread/90s configuration, did not hit — the window is tens
of nanoseconds wide; 4 hammers x 3min hit it.)

## Fixed kernel
the runner clears TASK_RUNNING under TQ_LOCK and issues wakeup() after
unlock; the sleeper's check and interlock are both under the same lock, so
the wakeup can no longer be lost. Bounded negative re-run (see
run.fixed.log): not hit.
