usb_proc_drain() cold-path calls suspend_kproc() on a thread that can never acknowledge the stop request β the suspend check is #if 0'd out and its macro calls a function that does not exist in DragonFly β leaving drain asleep forever in suspkp while holding the USB bus lock (boot-time wedge on controller attach failure)
Summary
The cold branch tsleeps forever waiting for a TDF_MP_STOPREQ acknowledgement the dispatcher can never give (its only suspend check is #if 0'd and kthread_suspend_check does not exist - the real primitive is kproc_suspend_loop), and suspend_kproc runs while drain still holds up->up_lock so the signalled thread can never re-acquire it either - a permanent double-wedge with the bus lock held. Harness: firing thread sat D1/suskp forever, WARNING never printed, second thread blocked on the held lock; guest needed a reset. Boot-time DoS on controller attach failure (broken xhci/ehci hardware or hypervisor config). kproc_suspend_loop + drop-lock-around-suspend fix validated (drain returns immediately).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5533 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| dfharness.c | β | 7.3 KB | view raw | |
| Makefile | β | 79 B | β download | |
| build.sh | β | 207 B | view raw | |
| run.sh | β | 310 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.patched.log | β | 1.6 KB | view raw | |
| env.txt | β | 864 B | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
DF-5533 β usb_proc_drain() cold-path: unconditional suspend_kproc() hang
File: sys/bus/u4b/usb_process.c (pass 2)
What this demonstrates
usb_proc_drain()'s cold-boot branch (usb_process.c:433-439):
if (cold) {
USB_THREAD_SUSPEND(up->up_ptr); /* suspend_kproc(p, 0) */
kprintf("WARNING: A USB process has been left suspended\n");
break;
}
calls suspend_kproc() (sys/kern/kern_kthread.c:188-204), which
tsleeps until the target thread acknowledges TDF_MP_STOPREQ β but
usb_process() never runs any suspend check: the intended
USB_THREAD_SUSPEND_CHECK() at usb_process.c:86-89 is #if 0'd out,
and the macro it expands to (kthread_suspend_check(curproc),
usb_process.c:60) does not exist anywhere in the DragonFly kernel
(the real primitive is kproc_suspend_loop(), used by bufdaemon/
syncer/vfs_mount). The drain thread therefore sleeps forever in
"suskp" while holding the USB bus lock, and the USB process thread
can never acquire the lock to exit either β permanent double-wedge.
Reachability: cold is true during autoconfiguration; a USB
controller attach failure reaching usb_proc_free()/usb_proc_drain()
(sys/bus/u4b/controller/usb_controller.c:210-236, 879-900 error path)
during boot wedges the boot with the bus lock held. Not attacker-
reachable in the standard configuration β this is a hardware/
hypervisor-triggered boot DoS (Low).
Build / run (on the QEMU guest, as root)
Same harness as DF-5532 (dfharness.c):
cd /root/dfharness && make && kldload ./dfharness.ko sleep 1; sysctl kern.dfstarv.stop=1 # quiesce the churn demo sleep 2 nohup sh -c 'sysctl kern.dfcold.fire=1; echo rc=$?' > /root/fire.log 2>&1 & sleep 3; cat /root/fire.log # EMPTY: never returned ps -axH -o pid,stat,wchan,comm | grep -E 'dfhz|sysctl' # -> sysctl thread in D-state, wchan "suspkp"; no WARNING printed
Expected (baseline, unpatched kernel)
1028 D1 suspkp sysctl # uninterruptible, forever fire.log empty (no return, and usb_process.c:437's WARNING never prints)
Fix
fix.diff (shared with DF-5532; validated in the same patched-kernel
build): (1) loop-top USB_THREAD_SUSPEND_CHECK() β kproc_suspend_loop()
(the existing DF daemon idiom), (2) release the bus lock around
suspend_kproc() so the target can actually run its suspend check.
DF-5533 VERDICT β REPRODUCED (dos, boot-time)
Bottom line
usb_proc_drain()'s cold-boot branch (sys/bus/u4b/usb_process.c:433-439)
calls USB_THREAD_SUSPEND(up->up_ptr) = suspend_kproc(td, 0)
(sys/kern/kern_kthread.c:188-204). suspend_kproc() sets
TDF_MP_STOPREQ on the target thread and tsleep(td, 0, "suspkp", 0)
β sleeping forever (timo=0) until the target acknowledges the stop
request. But the usb_process() dispatcher thread never acknowledges
anything:
- the only suspend check is
#if 0'd out at usb_process.c:86-89, and - even if it were live, the macro it expands to β
kthread_suspend_check(curproc)(usb_process.c:60) β does not exist in the DragonFly kernel (grep: zero definitions; the real primitive iskproc_suspend_loop(), kern_kthread.c:209).
Additionally, suspend_kproc() is called while the drain thread still
holds up->up_lock (acquired at usb_process.c:417) and its
tsleep(td, ...) variant releases no lock β so the USB process
thread, which usb_proc_drain() just cv_signal()ed at
usb_process.c:428-431, cannot re-acquire the lock to run its
up_gone exit either. Both threads freeze permanently; every later
taker of that USB bus lock joins the wedge.
Reproduction (QEMU guest, unpatched kernel)
Harness sysctl kern.dfcold.fire=1 (dfharness.c) sets cold=1 and
calls usb_proc_drain() on a live usb_process β the exact code path a
USB controller attach-failure takes during autoconfiguration via
usb_proc_free()/usb_proc_drain()
(sys/bus/u4b/controller/usb_controller.c:210-236, 879-900).
Observed (run.log):
- the
sysctl kern.dfcold.fire=1process went to D1 state, wait channel suspkp, and stayed there for the whole observation window (11+ s; it is still there at guest reset); /root/fire.logβ which captures the sysctl's return and theecho rc=$?β stayed empty:usb_proc_drain()never returned and thekprintf("WARNING: A USB process has been left suspended")at usb_process.c:437-438 never printed (it is after the never-returning call);- the
dfhzusb_process thread sat in B-state (blocked re-acquiring the bus lock the wedged drain thread holds); - the rest of the guest kept running: the wedge is bounded to the drain thread + the usb_process + future takers of that bus lock β which, at real boot time, includes the whole USB subsystem teardown path on that controller.
Impact
Boot-time denial of service, hardware/hypervisor-triggered (a USB
controller whose attach fails during cold autoconfiguration β faulty
hardware, or a QEMU/KVM configuration presenting a broken xhci/ehci).
Not remotely or locally attacker-triggerable in the default
configuration β Low severity, dos impact.
Fix validation
fix.diff (in this pack; shared with DF-5532's, validated in the same
patched-kernel build) does two things:
- loop-top
USB_THREAD_SUSPEND_CHECK();in the dispatcher, with the macro fixed to the real primitivekproc_suspend_loop()β the same idiom every DragonFly kernel daemon uses (vfs_bio.c:2251, vfs_sync.c:379, subr_prf.c:931, vfs_mount.c:529); - release
up->up_lockaroundsuspend_kproc()so the target thread can actually run its suspend check instead of deadlocking on the lock the suspender holds while sleeping.
On the patched kernel, sysctl kern.dfcold.fire=1 returns
immediately, the WARNING prints, and no D-state thread is left on
the sysctl path (the usb_process thread is intentionally left parked
suspended β exactly what the WARNING text promises).
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: fire sysctl in D1/suskp forever, never returns, WARNING never prints, bus lock held by wedged drain; patched kernel (full nativekernel build + reboot, fix.diff v2): 'sysctl kern.dfcold.fire=1' RETURNS immediately (FIRE-RETURNED-RC0), 'WARNING: A USB process has been left suspended' prints, no D-state drain thread remains, usb_process thread intentionally parked in kpsusp. Hang GONE.
findings/poc/DF-5533/run.patched.log (fire returns, WARNING printed, ps states; v1 deadlock capture), findings/poc/DF-5533/fix.diff, guest kernel #1 Oct 7 01:28:37
Confirmed kernel references
Detail
Exploit chain
faulty USB controller (or QEMU config presenting broken xhci/ehci) -> attach failure during cold autoconf -> usb_bus teardown error path -> usb_proc_free() -> usb_proc_drain() cold branch -> suspend_kproc() tsleeps forever holding the bus lock -> boot wedged (drain thread + usb_process thread + every later taker of that bus lock frozen)
Evidence (decisive lines)
run.log (sysctl PID in D1/suskp forever, fire.log empty, dfhz B3, WARNING absent), run.patched.log (fire returns RC0, WARNING printed, no D-state thread; includes the fix-v1 lock-parking deadlock capture that motivated v2), VERDICT.md, fix.diff (validated v2)
PoC changes
same KLD harness as DF-5532 plus a kern.dfcold.fire sysctl that sets cold=1 and calls usb_proc_drain() on the live usb_process (mirrors the controller-attach-failure path); fired via nohup background shell so the observing session survives the hang
Verified recommended fix
honour suspend requests in the dispatcher loop top with the bus lock dropped (kproc_suspend_loop idiom) and release up->up_lock around suspend_kproc() in drain's cold branch β see validated fix.diff
Verdict
usb_proc_drain()'s cold branch (usb_process.c:433-439) calls suspend_kproc() which tsleeps forever (timo=0, kern_kthread.c:188-204) until the usb_process thread acknowledges TDF_MP_STOPREQ β but the dispatcher's suspend check is #if 0'd out (usb_process.c:86-89) and the macro it would use calls kthread_suspend_check(), a function that does not exist anywhere in the DragonFly kernel; additionally suspend_kproc() is invoked while drain still holds the USB bus lock and its tsleep variant releases nothing, so the signalled usb_process thread can never re-acquire the lock to exit either. Reproduced on the guest: sysctl firing the cold-path drain went to D1/suskp and never returned (11s+ observation, still there at reset); the 'WARNING: A USB process has been left suspended' kprintf after the call never printed; dfhz blocked in B-state on the held lock. Boot-time DoS: reachable when a USB controller attach failure during cold autoconfiguration reaches usb_proc_free()/usb_proc_drain() (usb_controller.c:210-236, 879-900) β hardware/hypervisor-triggered, not attacker-triggerable in default config. Fix validated on a patched kernel (fix.diff v2): the fire sysctl returns immediately, the WARNING prints, no D-state drain thread remains, and the usb_process thread is left intentionally parked (kpsusp) exactly as the WARNING documents.
No comments yet.