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

ufoma mode-switch ACK waits are uninterruptible (1s link + 15s activate): unplug racing an open/close stalls ufoma_detach()'s usb_proc_drain on the USB hub event thread for up to ~16-18s per cycle

Summary

cfg_open/cfg_close end in cv_timedwait(hz / 15*hz) on the ucom cfg thread with no removal/drain abort - the sole wakeup is the vendor ACK notification, never signalled on unplug (intr xfer only cancelled): detach -> ucom_detach -> usb_proc_drain sleeps until the wait expires while the thread cannot observe up_gone, wedging the SYSTEM-WIDE USB hub event thread ~16-18s per cycle (1s LINK + 15s ACTIVATE + pause + 2x1s NAK timeouts). Evil MCPC device withholding ACKs + force-disconnect repeats at will; benign cable-yank racing open/close also triggers. Guest-verified via production-machinery KLD harness: drain blocked ELAPSED=13000/13005ms baseline, 0ms with the dying-flag+broadcast fix pattern. Slice-waits + wake-from-detach fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5367 Β· 13 files
FileTypeDescriptionSize
ufoma_drain_harness.c β€” 5.4 KB view raw
Makefile β€” 100 B ↓ download
build.sh β€” 173 B view raw
run.sh β€” 148 B view raw
build.log β€” 7.1 KB view raw
run.log β€” 781 B view raw
run.1.log β€” 781 B view raw
run.2.log β€” 725 B view raw
env.txt β€” 376 B view raw
fix.diff β€” 2.5 KB view raw
VERDICT.md β€” 4.8 KB ↓ raw
README.md β€” 3.1 KB ↓ raw
verdict.json β€” 4.5 KB view raw

DF-5367 β€” ufoma uninterruptible mode-switch ACK waits stall the USB hub event thread at detach (up to ~16-18 s)

What (finding in one line)

ufoma_cfg_open() / ufoma_cfg_close() block in cv_timedwait(&sc->sc_cv, &sc->sc_lock, …) for up to 1 s (ufoma_cfg_link_state, sys/bus/u4b/serial/ufoma.c:527) plus 15 s (ufoma_cfg_activate_state, ufoma.c:549-550 β€” UFOMA_MAX_TIMEOUT), with no abort when the device is removed or the ucom process is draining. The only wakeup source is the ACK notification handled in ufoma_intr_callback() (ufoma.c:689-697); on unplug the interrupt xfer is merely cancelled (ufoma.c:601,647,759) and nobody signals sc_cv. When unplug races an in-flight open/close cfg task, ufoma_detach() (ufoma.c:476) β†’ ucom_detach() β†’ usb_proc_drain() (usb_serial.c:394) blocks the USB hub event thread for the remainder of the wait β€” measured 13.005 s for an unplug 2 s into the 15 s wait β€” delaying all USB event processing system-wide, repeatable per insertion cycle.

Why this guest can't run the full path (evidence class)

ufoma probes on USB interface class UICLASS_CDC (0x02) / subclass UISUBCLASS_MCPC (0x88) (ufoma.c:114,314-317). The audit QEMU guest's usb-serial attaches but binds uftdi (proven in DF-5359) β€” no MCPC device can enumerate. Per the sanctioned evidence classes, verification is a KLD harness compiled from production code: it drives the same in-kernel usb_process machinery (usb_proc_create / usb_proc_msignal / usb_proc_drain) that ufoma_detach()'s ucom_detach() goes through, with a cfg task performing exactly ufoma.c:549's cv_timedwait(15*hz).

Build

# on the guest (DragonFly 6.5-DEVELOPMENT, /usr/src present):
mkdir -p /usr/src/sys/bus/u4b/serial/hx5367
cp ufoma_drain_harness.c Makefile /usr/src/sys/bus/u4b/serial/hx5367/
sh build.sh          # in-tree kmod build, -Werror clean

Run

sh run.sh            # kldload ./ufoharness.ko; kldunload; dmesg

Expected output (decisive lines)

HX: DF-5367 ufoma detach-stall harness, hz=100
HX[A] cfg-task: enter t=1791032834, cv_timedwait(15*hz), no abort on gone
HX[A] unplug done, usb_proc_drain() begin t=1791032836
HX[A] cfg-task: leave t=1791032849
HX[A] usb_proc_drain() done t=1791032849  ELAPSED=13000 ms ...
HX[B] cfg-task(fixed): enter t=1791032849, 1s slices + dying
HX[B] unplug done, usb_proc_drain() begin t=1791032851
HX[B] usb_proc_drain() done t=1791032851  ELAPSED=0 ms ...

Phase A (baseline) MUST show ELAPSED β‰ˆ (15 βˆ’ lead-in) seconds. Phase B (fix mechanism from fix.diff) MUST show ELAPSED β‰ˆ 0 ms. Run 2 confirmed stability: ELAPSED=13005 ms / 0 ms.

Artifacts

  • ufoma_drain_harness.c β€” the KLD harness (annotated with the ufoma.c lines it mirrors)
  • Makefile, build.sh, run.sh
  • build.log, run.log, run.1.log, run.2.log, env.txt
  • fix.diff β€” git-apply-able against sys/ (verified: git apply --check passes; the patched driver also compiles -Werror-clean as a kmod on the guest)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

VERDICT β€” DF-5367 (ufoma detach-stall)

status: reproduced (mechanism-level, production usb_process code) β€” impact: dos

Reproduced? Yes β€” how and why

The full end-to-end chain needs a crafted MCPC USB device (interface class 0x02 / subclass 0x88). This guest cannot attach one (usb-serial binds uftdi, established in DF-5359), so per the sanctioned evidence classes the PoC is a KLD harness built from production code that instantiates the exact kernel machinery the finding is about:

  • usb_proc_create() + usb_proc_msignal() β€” the same ucom cfg-task process (sc_tq) that ucom_attach() creates (usb_serial.c:322) and ucom_queue_command() feeds (usb_serial.c:635-659);
  • a cfg task whose body is ufoma.c:549-550 verbatim: cv_timedwait(&sc->sc_cv, &sc->sc_lock, UFOMA_MAX_TIMEOUT * hz) with no usb_proc_is_gone() / device-removal abort β€” exactly what ufoma_cfg_open() β†’ ufoma_cfg_link_state()/ufoma_cfg_activate_state() (ufoma.c:837-842) and ufoma_cfg_close() β†’ ufoma_cfg_activate_state() (ufoma.c:846-851) execute on the ucom cfg thread;
  • usb_proc_drain() β€” the exact drain ucom_detach() calls at usb_serial.c:394 on the ufoma_detach() path (ufoma.c:480).

Measured (run.log, run.2.log):

phase expectation run 1 run 2
A baseline: drain after "unplug" 2 s into the 15 s wait β‰ˆ 13 s 13000 ms 13005 ms
B fix mechanism (dying flag + cv_broadcast before drain) β‰ˆ 0 s 0 ms 0 ms

Why the mechanism is exact (kernel refs):

  • usb_proc_drain() sets up_gone and sleeps on up_cv until the process thread exits (usb_process.c:424-442);
  • the thread (usb_process.c:146-170) executes the callback to completion before it can ever re-check up_gone β€” a callback blocked in a 15 s cv_timedwait delays the drain by the full remainder;
  • the wakeup never comes from the driver side: the only cv_signal(&sc_cv) is in ufoma_intr_callback() (ufoma.c:696) for the vendor ACK notification; on unplug the intr xfer completes USB_ERR_CANCELLED and the callback simply returns (ufoma.c:601-602, 758-764).

So: unplug (or kldunload, or shutdown) racing an open/close within the ~2-18 s cfg-task window stalls ufoma_detach() β€” which runs on the USB hub event thread β€” for the remainder of the waits (1 s link + 15 s activate + 0.1 s pause, plus up to 2Γ—1 s control-request timeouts on a NAKing device). An evil device that withholds the ACK and force-disconnects on its own timing (a device can drop D+/Dβˆ’ at will) makes this deterministic per insertion cycle; repeatable cycles yield sustained USB event-thread starvation. Worst-case single-event block measured/derived: ~16-18 s.

Exploit chain

Not applicable (no memory corruption). Escalation ceiling: sustained system-wide USB hotplug/event denial plus a 16-18 s wedge of the hub event thread per cycle, triggered by an evil (or merely flaky) MCPC device plus any local open()/close() of the cuaU* node (any user; the callout nodes are world-accessible by default) β€” or simply an unplug racing a user's open()/close().

Fix validation

  • fix.diff (authored after verification): slices the ACK waits into 1 s pieces with a sc_dying abort, and ufoma_detach() sets sc_dying and cv_broadcast(&sc_cv) before ucom_detach(), so drain completes immediately.
  • git apply --check passes against the read-only sys/ tree.
  • The patched driver compiles -Werror-clean as a kmod on the guest (build output in the run transcript).
  • Harness Phase B runs the patched pattern against the same production drain machinery: 13000 ms β†’ 0 ms.
  • Full end-to-end patched-kernel validation is not possible on this guest (no MCPC device can attach; a rebuilt kernel would not change the harness, which exercises usb_process, not ufoma). Hence fix confidence is mechanism+compile level, not device level.

Honesty notes

  • The finding is bounded-time (≀ ~18 s per event) and requires either an evil/updated device or an unplug racing a tty open/close β€” Low severity, same weight class as DF-5359 (uipaq explore stall, 12.8 s, Low).
  • What was executed: production usb_process/usb_proc drain path with ufoma's exact wait shape (2 runs). What was traced but not executed: the device-side trigger (MCPC descriptor + ACK withholding + forced disconnect) β€” every link is cited in ufoma.c / usb_serial.c / usb_process.c and is deterministic scheduling, not speculation.

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 reproduced

fix.diff is git-apply-able against sys/ (git apply --check passes), the patched ufoma.c compiles -Werror-clean as a kmod on the guest, and harness Phase B runs the patched wait/detach pattern against the same production usb_proc drain machinery: drain 13000 ms -> 0 ms. Scope caveat: validation is mechanism+compile level; end-to-end with a physical crafted MCPC device is not possible on this guest (no such device can attach, DF-5359), and a rebuilt kernel would not alter the harness (it exercises usb_process, not ufoma).

fix.diff (sha256 55437f6b...); run.log HX[B] ELAPSED=0 ms lines; compile of patched driver in run transcript; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5367

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log / run.2.log lines 'HX[A] usb_proc_drain() done ... ELAPSED=13000 ms' (baseline) and 'HX[B] ... ELAPSED=0 ms' (fix); VERDICT.md traces every link: ufoma.c:527,549-550 waits -> only wakeup is intr ACK at ufoma.c:696, cancelled silently on unplug at ufoma.c:601/759 -> ucom_detach drain at usb_serial.c:394 -> usb_process.c:424-442 waits for thread exit -> usb_process.c:146-170 thread cannot observe up_gone inside the callback. build.log shows -Werror-clean production compile.

PoC changes

Original PoC sketch did not exist; authored from scratch. Three build iterations were needed: (1) Makefile must include (not ) as production modules do; (2) usb_process.h requires , , first (usbdi.h defines usb_proc_callback_t/usb_proc_msg); (3) SRCS needs generated opt_usb.h/device_if.h/bus_if.h; builds must run in-tree under /usr/src/sys/bus/u4b/serial/. Runtime logic needed no changes.

Verified recommended fix

Slice the ACK waits into 1 s cv_timedwait pieces with a sc_dying abort, and have ufoma_detach() set sc_dying + cv_broadcast(&sc_cv) before ucom_detach() (full diff in fix.diff)

Verdict

Mechanism-level reproduction with production code: a ucom cfg task executing ufoma.c:549-550's exact pattern (cv_timedwait(&sc_cv,&sc_lock,15*hz), no abort on device-gone) delays usb_proc_drain() -- the drain ucom_detach() performs at usb_serial.c:394 on the ufoma_detach() path -- by the full remaining timeout. Measured 13000 ms / 13005 ms across two runs for an unplug 2 s into the 15 s wait; fix mechanism (sc_dying + cv_broadcast before drain) measured 0 ms. End-to-end device trigger (crafted MCPC device, class 0x02/subclass 0x88, withholding the vendor ACK and force-disconnecting) is deterministic per the cited code but cannot attach on this guest (usb-serial binds uftdi, DF-5359), hence the KLD-harness evidence class. Impact ceiling: 16-18 s USB-hub-event-thread wedge per open/close-racing-unplug event, repeatable per insertion cycle by an evil device; no memory corruption.