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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5533 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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 is kproc_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=1 process 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 the echo rc=$? β€” stayed empty: usb_proc_drain() never returned and the kprintf("WARNING: A USB process has been left suspended") at usb_process.c:437-438 never printed (it is after the never-returning call);
  • the dfhz usb_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:

  1. loop-top USB_THREAD_SUSPEND_CHECK(); in the dispatcher, with the macro fixed to the real primitive kproc_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);
  2. release up->up_lock around suspend_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

fixed
baseline reproduced→ patch + rebuild →patched clean

baseline: 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
↓ fix.diffper-fix-DF-5533

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.