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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2870 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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_enqueuenever fires, and the onlywakeup(tq)source is the parked terminator.
Evidence
run.log:tqdead: RESULT: DEADLOCK CONFIRMED - taskqueue_free() stuck for 10+ secondsandps -axHshowingtqdeadctrlin 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
fixedCombined 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']
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.
No comments yet.