⬢ DragonFlyBSD Kernel Audit
DF-2873 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2873",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "Lost-wakeup race in taskqueue_drain()/drain_simple() reproduced by hammering: taskqueue_run() executes tq_running = NULL; wakeup(task); OUTSIDE the queue spinlock (subr_taskqueue.c:411-412) while the drainer checks ta_pending != 0 || task == tq_running under the spinlock (:493/:512) and then parks via ssleep() - because the waker never takes the spinlock, its wakeup can land between the drainers check and its sleep-queue interlock enqueue (kern_synch.c _tsleep_interlock called from ssleep at :821) and is lost forever. Guest proof: 4 hammer threads loop enqueue+drain on 4 tasks of a 1-worker queue for ~3 minutes; watchdog reports LOST WAKEUP REPRODUCED: hammer 3 frozen (h3 15165->15165) with ta_pending=0 and task not running and ps -axH shows lt_h3 permanently asleep on wchan - (the drain wmesg). A frozen drainer never returns - on a teardown path this is a permanent kernel-thread hang, reachable on ordinary busy queues without any taskqueue_free() (unlike DF-2870). Contrast: taskqueue_thread_enqueue (:629-642) is entered WITH the lock held, so its unlock->wakeup_one is interlock-ordered and that path cannot lose wakeups. Attempt 1 (2 hammers, 90 s) did not hit - window is tens of nanoseconds. fix.diff (runner clears per-task TASKQ_RUNNING under TQ_LOCK, wakeup after unlock; sleeper checks the flag under the same lock) validated by bounded negative re-run (4x4 hammer, 3 min): not hit, no frozen hammers.",
  "exploit_chain": "",
  "evidence": [
    "run.log: 'LOST WAKEUP REPRODUCED: hammer 3 frozen (h3 15165->15165) with ta_pending=0 and task not running'",
    "run.log ps: 'B2 - lt_h3' (permanently parked in drain)",
    "run.fixed.log: 'not hit in ~90s' + NO-FROZEN-HAMMERS + clean unload"
  ],
  "kernel_refs": [
    "sys/kern/subr_taskqueue.c:411",
    "sys/kern/subr_taskqueue.c:412",
    "sys/kern/subr_taskqueue.c:493",
    "sys/kern/subr_taskqueue.c:512",
    "sys/kern/subr_taskqueue.c:516",
    "sys/kern/kern_synch.c:821"
  ],
  "poc_changes": "watchdog loop condition originally used !hit_thr with hit_thr=-1 (no-op loop) - fixed to explicit 0/1/2 states before the recorded runs; first attempt (2 hammers/90 s) missed the window, second (4 tasks x 4 hammers/3 min) hit",
  "attempts": 3,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 6900,
  "guest_dirty": 0,
  "build_cmd": "cd /root/poc/tqlost && make -m /usr/share/mk SYSDIR=/usr/src/sys (see build.sh)",
  "run_cmd": "kldload /root/poc/tqlost/tqlost.ko; sleep 175; dmesg | grep tqlost; ps -axH -o stat,wchan,comm | grep lt_h (see run.sh)",
  "code_hash": "16d25065f58053efac9d1bae9716f02fff822de92ad5c6b3145086ad984f7228",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Wed Sep  2 17:34:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Combined fix.diff (per-task TASKQ_RUNNING under TQ_LOCK + wakeup after unlock in taskqueue_run; ACTIVE re-check before worker park; tq_callouts wait in taskqueue_free; timeout_func freer wakeup) built as kernel #1 Wed Sep 2 17:34:00. Baseline bad behavior GONE on every PoC: DF-2869 cancel=EBUSY + drain blocks + no UAF/panic + clean unload; DF-2870 taskqueue_free returns, no wedged threads; DF-2872 timeout task no longer runs after free; DF-2873 hammer does not lose a wakeup (bounded negative). Guest left clean via vm.sh reset with-src.",
  "fix_evidence": [
    "run.fixed.log"
  ],
  "notes": "fixed-kernel validation is a bounded negative (race absence cannot be proven, only the interlock ordering argument in fix.diff + failure to hit under the same hammer that hit on stock)",
  "recommended_fix": "Same discipline as DF-2869: clear the running-state under the queue spinlock and issue wakeup(task) only after unlocking, so the sleepers check-then-interlock (ssleep) is fully ordered against the waker."
}