β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2870

taskqueue_free() terminate lost-wakeup: worker parks without re-checking TQ_FLAGS_ACTIVE β†’ permanent deadlock with in-flight task

Field Value
ID DF-2870
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-667 (lost wakeup)
File sys/kern/subr_taskqueue.c
Lines 141-155, 617-619
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

taskqueue_terminate() delivers wakeup(tq) after TQ_UNLOCK. If the worker is executing ta_func at that moment the wakeup is lost; when the func returns, taskqueue_thread_loop() parks in TQ_SLEEP WITHOUT re-checking TQ_FLAGS_ACTIVE, and the terminator is already asleep on tq_threads β€” both sleep forever (post-free enqueues get EPIPE so nothing can wake them). 100% deterministic when taskqueue_free() is called with any in-flight task. Every in-tree taskqueue_free() consumer (wlan + ~15 net/storage drivers) wedges its calling thread permanently when tearing down a busy queue β€” detach-under-load local DoS. VERIFIED (findings/poc/DF-2870/tqdead.ko): "RESULT: DEADLOCK CONFIRMED β€” taskqueue_free() stuck for 10+ seconds" with both threads parked permanently. Fix (re-check ACTIVE under the held TQ_LOCK before parking) validated: free returns, clean unload.

Validated fix.diff in findings/poc/DF-2870/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_taskqueue.c (GLM 5.3); deterministic deadlock reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2870 Β· 12 files
FileTypeDescriptionSize
tqdead.c β€” 3.3 KB view raw
Makefile β€” 69 B ↓ download
build.sh β€” 315 B view raw
run.sh β€” 209 B view raw
build.log β€” 5.8 KB view raw
run.log β€” 397 B view raw
env.txt β€” 199 B view raw
fix.diff β€” 4.4 KB view raw
run.fixed.log β€” 262 B view raw
VERDICT.md β€” 2.5 KB ↓ raw
README.md β€” 1.9 KB ↓ raw
verdict.json β€” 3.7 KB view raw

DF-2870 β€” taskqueue_free() terminate lost-wakeup β†’ permanent deadlock

What

taskqueue_thread_loop() (subr_taskqueue.c:617-619) parks in TQ_SLEEP(tq, tq, "tqthr") after taskqueue_run() without re-checking TQ_FLAGS_ACTIVE. taskqueue_terminate() (subr_taskqueue.c:141-147) delivers wakeup(tq) after dropping the queue spinlock; if the worker is executing a task function at that moment (not on the sleep queue), the wakeup is lost. When the func returns, the worker parks unconditionally, the terminator is asleep on tq_threads, and nobody is left to wake either β€” taskqueue_free() never returns and the worker thread is leaked.

Trigger: taskqueue_free() called while any task of the queue is in flight (dequeued, ta_func executing). Every in-tree taskqueue_free() consumer (wlan ieee80211.c:464, ~15 network/storage drivers) reaches this window when tearing down a busy queue.

Build (on the guest as root)

same out-of-tree KLD pattern: see build.sh

Run

kldload /root/poc/tqdead/tqdead.ko
sleep 15
dmesg | grep tqdead
ps -axH -o stat,wchan,comm | grep -E "tqdead"

Expected output (stock kernel β€” reproduced)

tqdead: queue 0xfffff8008da44e88 started (0)
tqdead: task in-flight; calling taskqueue_free()...
tqdead: releasing in-flight task func
tqdead: RESULT: DEADLOCK CONFIRMED - taskqueue_free() stuck for 10+ seconds;
        ctrl_td=0xfffff8008df45b80 (wchan 0xfffff8008d560c80), worker parked forever
--- ps ---
B2     taskqueu tqdeadctrl     <- asleep in "taskqueue_terminate" forever
B4     tqthr    tqdead         <- worker parked forever

100% deterministic (no timing race needed β€” the harness blocks the func until after free() has parked).

NOTE: the wedged module cannot be unloaded; reset the guest afterwards.

Fixed kernel

worker re-checks TQ_FLAGS_ACTIVE before parking β†’ taskqueue_free RETURNED (not reproduced) and the module unloads cleanly (see run.fixed.log).

VERDICT.md
↓ download raw

DF-2870 VERDICT β€” REPRODUCED (permanent kernel deadlock, 100% deterministic)

One-line

taskqueue_free() called while a task is in-flight deadlocks forever: taskqueue_terminate()'s wakeup(tq) (subr_taskqueue.c:144, delivered after TQ_UNLOCK) is lost because the worker is inside ta_func and not on the sleep queue; when the func returns, taskqueue_thread_loop() parks in TQ_SLEEP(tq, tq) at :619 WITHOUT re-checking TQ_FLAGS_ACTIVE, and the terminator is already asleep on tq_threads (:146) β€” both sleep forever.

Interleaving (proven on the guest)

  • worker: dequeues task (ta_pending=0), releases TQ_LOCK, executes ta_func (harness blocks it on a flag) β€” not on any sleep queue for ident tq.
  • freer: taskqueue_free() β†’ TQ_LOCK, clears TQ_FLAGS_ACTIVE (:154), taskqueue_run(queue,1) returns instantly (queue empty β€” the in-flight task was already dequeued), taskqueue_terminate(): TQ_UNLOCK β†’ wakeup(tq) β†’ lost (worker not sleeping) β†’ TQ_LOCK β†’ parks in TQ_SLEEP(pp).
  • harness releases the func; worker: wakeup(task), TQ_LOCK, run loop sees empty queue, parks in TQ_SLEEP(tq, tq) β€” without re-checking ACTIVE.
  • Both threads sleep with no timeout; nobody can wake them: enqueues are rejected (EPIPE, :199) so taskqueue_thread_enqueue never fires, and the only wakeup(tq) source is the parked terminator.

Evidence

  • run.log: tqdead: RESULT: DEADLOCK CONFIRMED - taskqueue_free() stuck for 10+ seconds and ps -axH showing tqdeadctrl in wchan "taskqueue_terminate" (truncated "taskqueu") and the worker in "tqthr", both permanently.
  • The escape case (worker parked before free) is the properly-drained case; the deadlock hits whenever a task is in-flight at free time β€” the exact situation free()'s run+terminate machinery exists to handle (subr_taskqueue.c:153-156).

In-tree reachability

Every taskqueue_free() consumer (wlan ieee80211.c:464; ~15 network and storage drivers in the callers list) that frees a still-active queue wedges the calling thread forever β€” a detach-path hang under load (local DoS; also remotely-triggerable if the detach is event-driven, e.g. device removal during traffic).

Fix validation

fix.diff: worker re-checks TQ_FLAGS_ACTIVE (under the still-held TQ_LOCK) between taskqueue_run() and TQ_SLEEP, exiting instead of parking. On the fixed kernel the harness prints taskqueue_free RETURNED (not reproduced) and the module unloads cleanly. See run.fixed.log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

['run.fixed.log']
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 17:34:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 'RESULT: DEADLOCK CONFIRMED - taskqueue_free() stuck for 10+ seconds'", "run.log ps: 'B2 taskqueu tqdeadctrl' + 'B4 tqthr tqdead' (both parked forever)", "run.fixed.log: 'taskqueue_free RETURNED (not reproduced)' + NO-TQDEAD-THREADS + clean unload"]

PoC changes

initial DECLARE_MODULE build GPFs this guest's kldload (loader quirk; DEV_MODULE used instead); harness watchdog originally mis-initialized (hit_thr=-1 made the loop a no-op) - fixed before the recorded run

Verified recommended fix

Re-check TQ_FLAGS_ACTIVE under the queue spinlock between taskqueue_run() and TQ_SLEEP in taskqueue_thread_loop() so a wakeup delivered while ta_func runs cannot be lost.

Verdict

100% deterministic permanent kernel deadlock. taskqueue_free() called while any task of the queue is in-flight never returns: taskqueue_terminate()'s wakeup(tq) (subr_taskqueue.c:144, fired after TQ_UNLOCK) lands while the worker executes ta_func (not on the sleep queue) and is lost; when ta_func returns, taskqueue_thread_loop() parks in TQ_SLEEP(tq,tq) at :619 without re-checking TQ_FLAGS_ACTIVE, and the terminator is already asleep on tq_threads at :146 - both sleep forever with no timeout and no remaining waker (post-free enqueues return EPIPE at :199, so taskqueue_thread_enqueue never fires). Guest proof: harness blocks ta_func on a flag, calls taskqueue_free, releases the func from a third thread; RESULT: DEADLOCK CONFIRMED - free stuck 10+s, ps -axH shows tqdeadctrl in wchan 'taskqueue_terminate' and the worker in 'tqthr' permanently. Every in-tree taskqueue_free() consumer (wlan ieee80211.c:464 plus ~15 drivers) wedges its calling thread this way when freeing a still-active queue (detach under load -> local DoS). fix.diff (re-check TQ_FLAGS_ACTIVE under the held spinlock between run and sleep) validated: 'taskqueue_free RETURNED (not reproduced)', no wedged threads, module unloads cleanly.