# DF-2869 VERDICT — REPRODUCED (panic; memory-corruption primitive proven)

## One-line
The single `tq_running` slot (set at subr_taskqueue.c:407, cleared *outside*
the lock at :411) makes `taskqueue_cancel` (:429) and `taskqueue_drain`
(:493) blind to concurrently-running tasks whenever ≥2 threads execute
`taskqueue_run()` on one queue; the documented cancel/drain-then-free
contract then frees a task whose `ta_func` is still executing — proven as a
deterministic use-after-free with a panic backtrace
`taskqueue_thread_loop → taskqueue_run → t1_func` on freed-and-reallocated
memory.

## How it reproduces (100% deterministic, no timing needed)
1. `taskqueue_start_threads(&tq, 2, ...)` — an API-supported configuration
   (subr_taskqueue.c:548, thread distribution across CPUs at :580-581).
2. Worker A dequeues T1 (slow func: blocks on a flag). `tq_running = T1`.
3. Worker B dequeues T2 (instant). On completion B executes
   `queue->tq_running = NULL; wakeup(T2);` — while T1 is still running.
4. `taskqueue_cancel(tq, &T1)` returns **0** (should be EBUSY) —
   `T1 != tq_running(NULL)`.
5. `taskqueue_drain(tq, &T1)` returns **immediately** — same check at :493.
6. Caller frees T1's context (the documented pattern), kfree + kmalloc the
   same size (objcache LIFO ⇒ same address, printed "alias YES").
7. Controller pokes `ctx->release` (UAF write); T1 resumes and reads
   `ctx->magic == 0xdeadbeef` → `panic("DF-2869: ...")`.

## Evidence
- `run.log` / `panic.txt`: the full sequence above, including
  `tquaf: taskqueue_cancel(t1) = 0 while t1 running` and
  `tquaf: BUG PROVEN: taskqueue_drain(t1) returned while t1 ta_func still executing`
  and the panic trace through `taskqueue_run+0xbb → t1_func+0x9b`.

## Primitive characterization (write-capable bug)
The premature-free gives the attacker's teardown path a "free a live kernel
object while a CPU is still executing its handler" primitive. In the harness
the victim is a 16-byte kmalloc'd context; in real consumers the victim is
driver-owned state (e.g. vmxnet3's tx queue drained at if_vmx.c:1012 while
its deferred-rx/tx taskqueue runs ≥2 threads, if_vmx.c:990-997). The
reallocated memory is read AND written by the running handler before it
returns (`entered`/`release`/`magic`), i.e. a slab-groomable UAF read/write
of task-owned objects. Escalation to uid=0 requires a consumer whose task
context embeds a pointer that the still-running handler dereferences or
stores through after the free; with heap grooming (same-zone reallocation)
this is the standard "free-while-running" corruption family. No
unprivileged in-tree trigger exists on this guest (vmxnet3 teardown needs
root; the queue must be a count≥2 or a free-racing queue), so the uid0
chain was not exercised end-to-end — that is a hard blocker of *reach*,
not of the primitive, which is proven here.

## Who else co-runs a queue (reachability)
- Any `count>=2` queue: API-documented, vmxnet3 in-tree.
- `taskqueue_free()` itself co-runs the queue with the worker
  (subr_taskqueue.c:155 calls taskqueue_run while the worker may be inside
  its own run loop) — every thread-based taskqueue gets transient
  multi-runner windows during teardown.
- SWI queues are safe from this variant (single global ithread dispatches
  them, kern_intr.c sched_ithd_soft).

## Fix validation
Combined fix (fix.diff in this pack): per-task `TASKQ_RUNNING` flag set at
dequeue and cleared under TQ_LOCK; `wakeup(task)` after unlock; cancel/drain
consult the flag. On the fixed kernel: `taskqueue_cancel` returns EBUSY(16),
`taskqueue_drain` blocks until the func completes, the harness takes its
"NOT REPRODUCED: drain blocked correctly" path, no panic. See run.fixed.log.
