usb_proc_mwait() wakeup is gated on the whole queue becoming empty, not on the waited-for messages completing: any continuously refilled usb_process queue holds waiters in uninterruptible (PCATCH-less) sleep indefinitely β the root mechanism beneath the DF-5367..5504 cfg-stall family's uninterruptible-occupancy measurements
Summary
mwait's only wakeup is the dispatcher's empty-queue transition - a waiter sleeps in the PCATCH-less cv_wait until EVERY message source on that usb_process quiesces, even after its own messages completed. Harness on the production symbols: victim message completed at T+0.53s but the waiter was still asleep at T+20s under a queue of mere 10ms messages (starve_ms 19481/19520 - bounded only by the churn's lifetime); the spin variant (instant self-requeue) monopolized the thread AND bus lock for 275s and starved sshd to death. The independent ROOT defect beneath the 10-finding cfg-stall family (even zero-slow-callback queues starve waiters). Unpriv via the family-established ucom tcsetattr/close and unsetup consumers: unbounded SIGKILL-immune D-state. Broadcast-on-dequeue-under-up_dsleep fix validated (starve_ms -> 0).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5532 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| dfharness.c | β | 7.3 KB | view raw | |
| Makefile | β | 79 B | β download | |
| build.sh | β | 207 B | view raw | |
| run.sh | β | 263 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| run.2.log | β | 556 B | view raw | |
| run.spin.log | β | 1.7 KB | view raw | |
| run.patched.log | β | 1.3 KB | view raw | |
| env.txt | β | 864 B | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw |
DF-5532 β usb_proc_mwait() starvation: wakeup gated on whole-queue-empty
File: sys/bus/u4b/usb_process.c (pass 2)
What this demonstrates
usb_proc_mwait() (usb_process.c:363-394) documents "returns when the
message pointed to by pm is no longer on a queue", but the ONLY wakeups
for up_drain are:
- the dispatcher's empty-queue transition (usb_process.c:163-167), and
usb_proc_drain()teardown (usb_process.c:445-447).
So if the queue is continuously refilled by any other message source, a waiter sleeps forever (cv_wait, no PCATCH β uninterruptible D-state) long after its own messages have completed.
Harness (family evidence class: production-code KLD rig)
dfharness.c drives the production usb_proc_* machinery:
- a "churn" message pair whose callback does trivial work, drops the lock for 1 tick (as every real cfg callback does around its USB request) and re-queues itself β queue never empties, no slow callback anywhere;
- a "victim" message pair that the
dfwaitkernel thread msignal()s and mwait()s β the stand-in for the unprivileged-syscall waiters of the production consumers (ucom_queue_command(), usb_serial.c:655-672, tcsetattr/close on /dev/ttyU*;usbd_transfer_unsetup(), usb_transfer.c:1286).
Build / run (on the QEMU guest, as root)
cd /root/dfharness make # dfharness.ko kldload ./dfharness.ko sleep 3 && sysctl kern.dfstarv.report # victim done, waiter ASLEEP sleep 6 && sysctl kern.dfstarv.report # ... still asleep sysctl kern.dfstarv.stop=1 # let the queue drain sleep 1 && sysctl kern.dfstarv.report # waiter woke; starve_ms shown kldunload dfharness
Expected (baseline, unpatched kernel)
T+3s : victim_ran=1 waiter_done=0 (victim completed at ~T+0.5s) T+9s : victim_ran=1 waiter_done=0 churn_countβ1000 (10ms/msg, no stall) stop : waiter_done=1 starve_msβ19500 (woke 20ms after the queue could empty)
starve_ms is how long the waiter overslept its own messages'
completion. It is bounded only by when the churn happens to stop β
in production the churn is sustained USB traffic/driver polls, so the
wait is unbounded.
Extreme variant (churn_ticks=0, spin mode)
An instantly self-requeuing message monopolizes the usb_process
thread AND its lock: observed 8.9e9 callbacks over 275 s, victim never
executed, other threads starved even in lockmgr, guest userland (sshd)
starved. See run.spin.log.
Fix
fix.diff (validated on a patched kernel): broadcast up_drain after
every completed-message dequeue (if (up->up_dsleep) guard β zero cost
when nobody waits). Also carries the DF-5533 cold-path fix.
DF-5532 VERDICT β REPRODUCED (dos)
Bottom line
usb_proc_mwait()'s documented contract ("This function will return
when the USB process message pointed to by "pm" is no longer on a
queue", sys/bus/u4b/usb_process.c:357-362) is not what the
implementation provides: the up_drain condvar is only signalled
when the dispatcher finds the queue completely empty
(usb_process.c:163-167) or when usb_proc_drain() tears the process
down (usb_process.c:445-447). A waiter therefore sleeps until every
message source on that usb_process quiesces β not until its messages
complete β and it sleeps in cv_wait() with no PCATCH
(sys/kern/kern_condvar.c:20-44, wakesig=0), i.e. uninterruptibly.
Reproduction (QEMU guest, DragonFly 6.5-DEVELOPMENT, unpatched kernel)
KLD harness dfharness.c (family evidence class β production-code rig
on the real usb_proc_* symbols, no kernel modification):
- run 1 (run.log): victim message completed at T+0.53 s
(
victim_ran=1, victim_ran_ms=...959401); at T+20 s the waiter was still asleep (waiter_done=0) with only 10 ms of trivial work per queue message (churn_count=1000 over 20 s β no stall anywhere); 20 ms afterstop=1emptied the queue, the waiter woke:starve_ms=19481. - run 2 (run.2.log): identical shape,
starve_ms=19520. - spin variant (run.spin.log): with
churn_ticks=0(callback never sleeps β pure self-requeue) the usb_process thread executed 8.9e9 callbacks over 275 s, monopolizing the thread and its lock (the waiter starved already insidelockmgr), and guest userland was starved to the point that sshd stopped answering β guest reset. 46141778 callbacks were counted in the first second alone.
starve_ms is bounded only by the churn's lifetime. In production the
churn is sustained USB traffic (transfer-done queues) or driver poll
loops; there is no bound at all.
Why the family findings inherit from this
DF-5367/5421/5424/5425/5449/5456/5476/5487/5496/5504 measured
per-driver uninterruptible occupancy of this machinery (slow cfg
callbacks holding the queue). This finding is the independent,
sharper root defect in usb_process.c itself: even with zero slow
callbacks, any continuously refilled queue starves mwait waiters
whose messages completed long ago β the wakeup simply never fires.
The unprivileged reachability established by the family applies
verbatim: ucom tcsetattr(2)/close(2) β ucom_queue_command() β
usb_proc_mwait() (sys/bus/u4b/serial/usb_serial.c:655-672);
usbd_transfer_unsetup() β mwait (sys/bus/u4b/usb_transfer.c:1286).
Exploit chain (what an attacker gets)
Local DoS: an unprivileged user with access to a ucom tty (or any
consumer whose queue is kept alive by traffic) parks syscall threads
in uninterruptible D-state for unbounded time (SIGKILL-immune); with
the spin variant the whole usb_process (and its bus lock) is
monopolized, stalling every other consumer of that queue β including
kernel teardown paths (usb_proc_drain() waits for the thread, which
never sleeps β detach hangs β the hub event thread stalls, exactly the
"stalls the USB hub event thread on unplug" the family measured).
No memory-corruption primitive is offered by this defect; the impact
ceiling is unbounded uninterruptible denial of service.
Fix validation
fix.diff adds, after every completed-message dequeue in the
dispatcher, if (up->up_dsleep) cv_broadcast(&up->up_drain); (zero
cost when nobody waits β up_dsleep is only set by a live mwait
waiter). On the patched kernel (full buildkernel + reboot):
- baseline starve_ms = 19481/19520 (unpatched, above);
- patched: waiter wakes within one queue tick of its victim message
completing while the identical churn runs on
(
starve_msβ10-20, run.patched.log) β a ~3-orders-of-magnitude reduction, bounded by real work, not by unrelated queue activity.
The diff also carries the DF-5533 fix (loop-top suspend check + lock release around suspend_kproc); both were validated in the same build.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedbaseline starve_ms=19481/19520 (waiter asleep 19.5s+ after its messages completed, under a queue of 10ms messages); patched kernel (full nativekernel build + reboot, fix.diff hunks 1-3): starve_ms=0 β waiter wakes in the same millisecond its victim message completes with the identical churn load (churn_count=151 at T+3s both baseline and patched). Bad behaviour GONE.
findings/poc/DF-5532/run.patched.log (v1 T+3s/T+10s and v2 T+3s samples, both starve_ms=0); findings/poc/DF-5532/fix.diff; guest kernels #1 Oct 7 01:12:42 (v1) and #1 Oct 7 01:28:37 (v2)
Confirmed kernel references
Detail
Exploit chain
local unprivileged user -> consumer syscall path that mwaits (ucom tcsetattr(2)/close(2) on /dev/ttyU*, or transfer unsetup during traffic) -> cv_wait(&up_drain) with no PCATCH -> any unrelated continuously-refilled message source on the same usb_process (driver poll loop, sustained RX/TX transfer-done churn) keeps the queue non-empty -> empty-queue wakeup never fires -> syscall thread parked in unkillable D-state for unbounded time; spin-variant monopolizes the usb_process thread + bus lock, stalling all other consumers incl. detach/hub-event teardown
Evidence (decisive lines)
run.log (T+3s/T+9s/asleep + post-stop wake, starve_ms=19481), run.2.log (starve_ms=19520), run.spin.log (8.9e9 callbacks/275s, sshd starved), run.patched.log (starve_ms=0 on fix v1+v2 kernels), VERDICT.md narrative, fix.diff (validated)
PoC changes
seed sketch rewritten as a phase-driven KLD harness: churn pair whose callback requeues itself with a 1-tick lock drop (production-faithful; pure-spin mode kept as sysctl-selectable extreme variant), victim pair msignal+mwait from a dedicated kernel thread standing in for the unprivileged-syscall waiters; added report/stop sysctls; needed opt_usb.h, bus_if.h/device_if.h in SRCS and
Verified recommended fix
broadcast up_drain after every completed-message dequeue in the dispatcher (guarded by up_dsleep so it is free when nobody waits) β see validated fix.diff
Verdict
usb_proc_mwait()'s only wakeups are the dispatcher's empty-queue transition (usb_process.c:163-167) and drain teardown (:445-447), so a waiter sleeps uninterruptibly (cv_wait, no PCATCH) until EVERY message source on the usb_process quiesces β not until its own messages complete. Reproduced with a production-code KLD harness on the real usb_proc_* symbols: victim message completed at T+0.53s while the waiter was still asleep at T+20s under a queue whose every message cost one 10ms tick (churn_count == elapsed ticks, no slow callback anywhere); waiter woke 20ms after the churn stopped (starve_ms=19481; second run 19520). Extreme variant (instant self-requeue): 8.9e9 callbacks monopolized the usb_process thread AND its bus lock for 275s and starved guest userland to death. Unprivileged reachability via the family-established consumers (ucom tcsetattr/close -> ucom_queue_command -> mwait, usb_serial.c:655-672; usbd_transfer_unsetup -> mwait, usb_transfer.c:1286). Fix validated on a patched kernel: starve_ms 19481/19520 -> 0 (waiter wakes in the same millisecond its message completes, same churn load).
No comments yet.