# DF-2872 VERDICT — REPRODUCED (post-free UAF via armed timeout callout)

## One-line
`taskqueue_free()` (subr_taskqueue.c:150-166) never checks
`queue->tq_callouts` — the counter it exists for is write-only (:359
increment, :335/:480 decrement, zero readers) — so freeing a queue with an
armed `timeout_task` callout leaves a live callout pointing at freed
memory: when it fires, `taskqueue_timeout_func()` (:323-338) locks the
freed queue's spinlock, decrements its `tq_callouts`, and STAILQ-inserts
its task into the freed queue.

## How it reproduces (100% deterministic)
1. create q1, `TIMEOUT_TASK_INIT`, `taskqueue_enqueue_timeout(q1, &tt, 5)`.
2. `taskqueue_free(q1)` — tq_callouts==1, ignored; queue kfree'd.
3. Immediately `taskqueue_create()` (same zone, same size, objcache LIFO):
   **q2 lands on q1's exact address** (iteration 0; printed alias YES).
4. 5 ticks later the callout fires: `tt.t.ta_queue` == freed q1 == live q2;
   the kernel enqueues `tt` onto q2 (a queue that never saw it), q2's
   worker executes `tt_func` — on the wrong, reallocated-from-freed queue.
5. Additional writes into the freed-then-reused object during the callout:
   `q->tq_callouts--` (live q2 corrupted to -1), `f &= ~DT_CALLOUT_ARMED`,
   `STAILQ_INSERT_TAIL(&q->tq_queue, ...)`, `spin_lock(&q->tq_lock)`.

## Evidence
- `run.log`: `alias of freed q1: YES` + `TIMEOUT TASK RAN (run 1) on queue
  0xfffff80117bcd180 (q1 was ...180, q2 is ...180) <== REALIASED (FREED)
  QUEUE: UAF CONFIRMED`.
- No panic was coaxed (the corrupted fields are not lethal in this shape),
  so impact is recorded dos-class with the write primitives enumerated
  above — same convention as DF-2848 (silent heap corruption).

## In-tree hazard
wlan: `TIMEOUT_TASK_INIT(ic->ic_tq, &ss->ss_scan_curchan, ...)`
(ieee80211_scan_sw.c:1023) on the queue that is `taskqueue_free`'d at
ieee80211.c:464; `ieee80211_scan_detach()` never cancels it. A scan in
progress at interface destroy arms exactly this UAF (with the *scan state
itself* also freed at scan_detach — a second UAF on the callout argument).

## Fix validation
fix.diff: `taskqueue_free()` waits for `tq_callouts == 0` after clearing
TQ_FLAGS_ACTIVE (completing callouts then fail their enqueue with EPIPE),
and `taskqueue_timeout_func()` wakes the freer when the count reaches 0.
On the fixed kernel the harness reports the timeout task did NOT run after
free ("callout did not run in 5s?!" — the correct fixed behavior) and
unloads cleanly. See run.fixed.log.
