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

uslcom cfg callbacks chain 3+1+1 sequential 1000 ms-timeout EP0 vendor requests with discarded errors: evil CP210x stretches every tcsetattr to ~3.1 s and every open/param/close cycle to ~5.2 s of uninterruptible ucom-taskqueue occupancy, blocking close(2) (~8.2 s with two queued tsetattrs) and stalling the USB hub event thread on unplug (user-amplifiable)

Summary

cfg_param chains three sync 1000ms-timeout vendor requests per tcsetattr (open/close +1 each) with every ucom_cfg_do_request return discarded ('failed (ignored)') - no chain aborts: an evil CP210x-clone (80+ VID/PID table) makes tcsetattr ~3.1s and open+set+close ~5.2s of uninterruptible taskqueue occupancy, ~8.2s with two queued tsetattrs (dialer-user amplifiable), plus the hz/4 watchdog's RCTRL xfers contending the shared ep0 queue. The DF-5367 family mechanism. Harness-proven with the production usb_proc machinery: 5078/5090ms (1 tsetattr) and 8141/8094ms (2) baseline; abort-on-first-error 3149/3121ms; abort+250ms-timeout 899/925ms. Abort + bounded-cfg-timeout fix, compile-validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5425 Β· 14 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 8.3 KB ↓ raw
uslcom_cfgstall_harness.c β€” 11.2 KB view raw
Makefile β€” 108 B ↓ download
build.sh β€” 159 B view raw
run.sh β€” 154 B view raw
build.log β€” 7.1 KB view raw
run.log β€” 2.9 KB view raw
run.2.log β€” 2.9 KB view raw
fix.diff β€” 2.6 KB view raw
fix_compile.log β€” 7.3 KB view raw
env.txt β€” 331 B view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 6.8 KB view raw

DF-5425 β€” uslcom cfg-chain EP0 stall (evil CP210x)

What (human-readable)

sys/bus/u4b/serial/uslcom.c issues sequential synchronous EP0 vendor control requests from its ucom cfg callbacks and discards every result:

  • uslcom_param() (uslcom.c:505-583) β€” fired on every tcsetattr of an open tty β€” chains 3 requests: USLCOM_BAUD_RATE divisor write (uslcom.c:514-523, request issued at 520), USLCOM_DATA format write (uslcom.c:551-560, at 557), USLCOM_SET_FLOWCTRL 16-byte out (uslcom.c:573-582, at 579).
  • uslcom_open() (uslcom.c:406-425) β€” every open(2) β€” adds 1 (USLCOM_UART enable, at 418).
  • uslcom_close() (uslcom.c:427-446) β€” every close(2) β€” adds 1 (USLCOM_UART disable, at 442).
  • Every request is ucom_cfg_do_request(..., 1000) with a 1000 ms timeout, and every call site throws the usb_error_t away β€” "failed (ignored)" at uslcom.c:420, 522, 559, 581, 444 β€” so no chain step ever aborts.

An evil CP210x-clone (probe is VID/PID-table only, uslcom.c:208-293/328-345; 80+ VID:PID pairs) that simply never answers the control data/status stage makes each request eat its full timeout (usb_request.c:642-644 "WXFER" sleep loop, 699-704 timeout) and the chain marches on: ~3.1 s per tcsetattr, ~1.1 s per open, ~1.0 s per close. A device that answers at just-under-timeout makes every request succeed slowly β€” same cost, and no error to abort on.

uslcom amplifier: while the port is open, the watchdog (uslcom.c:315-326, rearmed at hz/4 by uslcom_open:424) keeps an RCTRL control xfer in flight on the shared ep0 endpoint queue (uslcom.c:756-770, 1 s timeout), so cfg requests can queue behind up to ~1 s of in-flight status polling β€” the measured floors below do not include this.

Who blocks

  • The chain runs on the per-adapter ucom taskqueue (sc_super_ucom.sc_tq, usb_serial.c:322) holding sc->sc_lock.
  • close(2) is synchronous against that queue: ucom_close() β†’ ucom_queue_command(ucom_cfg_close) β†’ usb_proc_mwait() (usb_serial.c:671-672, 955-957), an uninterruptible cv_wait (usb_process.c:385-393); a message stays head-of-queue for its whole callback (usb_process.c:146-158).
  • Unplug stalls the USB hub event thread the same way via ucom_detach() β†’ usb_proc_drain() (usb_serial.c:394) β€” the DF-5367 mechanism, uslcom instance.
  • Any local user with the tty open (/dev/cuaU* is uucp:dialer 0660, usb_serial.c:483-488) multiplies the occupancy: each tcsetattr re-queues the param task into ucom's double buffer (usb_serial.c:1691-1693) β€” two outstanding tasks β‰ˆ ~8.2 s of uninterruptible close(2)/unplug block, repeatable indefinitely.

Reproduce (no CP210x device model exists on the QEMU guest β†’ KLD harness)

Same evidence class as DF-5367/DF-5383/DF-5421: the harness drives the real in-kernel usb_process machinery (create/msignal/mwait/free β€” the exact taskqueue plumbing of ucom's synchronous close and detach drain) with uslcom's production chain shapes (1 req/open-task + hz/10, 3 reqs/param-task + hz/10, 1 req/close-task, 1000 ms/request).

# on the guest
sh build.sh          # in-tree kmod build, -Werror clean -> uslcomcfgstall.ko
sh run.sh            # kldload (phases run at MOD_LOAD), kldunload, dmesg

Expected decisive lines (two runs measured):

Phase Meaning run 1 run 2
[A] open + 1 tcsetattr + close (baseline) 5078 ms 5090 ms
[2] open + 2 tcsetattrs + close (user-amplified) 8141 ms 8094 ms
[B] fix #1: abort chain on first failed request 3149 ms 3121 ms
[C] fix #1+#2: abort + 250 ms cfg timeout bound 899 ms 925 ms

Fix

fix.diff (git-apply-able against sys/, verified): (1) uslcom_param() aborts on the first failed request (baud/format/flow are one unit); (2) all cfg-path control timeouts bounded 1000 β†’ 250 ms via USLCOM_CFG_TIMEOUT (open/close/ dtr/rts/break/param) β€” the ioctl path keeps 1000 ms since it only prices the caller's own thread. The patched production driver compiles -Werror-clean as the in-tree uslcom.ko (fix_compile.log).

Impact

Availability-only: local unprivileged user (dialer group) + physical evil USB device β‡’ uninterruptible multi-second close(2)/unplug stalls, repeatable; per-family calibration = Low (DF-5367/DF-5383/DF-5421/DF-5424).

VERDICT.md
↓ download raw

DF-5425 β€” VERDICT

Status: reproduced (impact=dos, confidence=certain). Mechanism-level reproduction with production machinery (KLD-harness-with-production-code evidence class β€” no CP210x device model exists on the QEMU guest).

What was verified and how

1. The chain (source, line-accurate)

uslcom_param() (uslcom.c:505-583) issues three sequential synchronous EP0 vendor requests per tcsetattr of an open tty:

# request issued at error handling
1 USLCOM_BAUD_RATE divisor write uslcom.c:520-523 discarded β€” "Set baudrate failed (ignored)" (522)
2 USLCOM_DATA format write uslcom.c:557-560 discarded β€” "Set format failed (ignored)" (559)
3 USLCOM_SET_FLOWCTRL 16-byte OUT uslcom.c:579-582 discarded β€” "Set flowcontrol failed (ignored)" (581)

uslcom_open() (uslcom.c:418-421) adds one USLCOM_UART enable per open(2); uslcom_close() (uslcom.c:442-445) adds one disable per close(2) β€” uslcom, unlike uchcom, has a ucom_cfg_close callback, so the close task itself costs a full request timeout. All go through ucom_cfg_do_request(..., 1000) β†’ usbd_do_request_proc (usb_request.c:759-790) β†’ usbd_do_request_flags, whose "WXFER" loop (usb_request.c:642-644) sleeps uninterruptibly until the transfer completes or the 1000 ms timeout fires (usb_request.c:699-704). A control transfer's data/status stage can be NAK'd by the device indefinitely (USB 2.0 protocol level), so a silent evil CP210x pins each request to its full timeout and the chain marches on β€” no error abort exists anywhere in the chain.

2. Who blocks (source)

  • Per-adapter ucom taskqueue: sc_super_ucom.sc_tq (usb_serial.c:322).
  • Synchronous uninterruptible close: ucom_close() queues ucom_cfg_close then usb_proc_mwait() (usb_serial.c:955-957 with 671-672); cv_wait semantics (usb_process.c:385-393); a queued message stays head-of-queue for the whole callback (usb_process.c:146-158).
  • Unplug: ucom_detach() β†’ usb_proc_drain() (usb_serial.c:394) makes the USB hub event thread wait out the same chain β€” the DF-5367 mechanism, uslcom instance.
  • Local amplification: each ioctl(TIOCSETA) on an open tty re-queues the param task into the sc_param_task[2] double buffer (usb_serial.c:1691-1693); /dev/cuaU* is uucp:dialer 0660 (usb_serial.c:483-488).
  • uslcom-specific amplifier: the open-time watchdog (uslcom.c:315-326, rearmed hz/4 at uslcom.c:424) keeps an RCTRL control xfer in flight on the shared ep0 queue (uslcom.c:756-770, 1 s timeout) β€” each cfg request can additionally wait behind up to ~1 s of in-flight status polling. Not counted in the measurements below (they are floors).

3. Reproduction (harness on production machinery)

uslcom_cfgstall_harness.c builds the exact production shapes on the real usb_proc_create/msignal/mwait/drain machinery: cfg_open = 1Γ—1000 ms + hz/10 pause (usb_serial.c:777), cfg_param = 3Γ—1000 ms + hz/10 pause (usb_serial.c:1636), cfg_close = 1Γ—1000 ms (no core pause), and measures the closer thread's usb_proc_mwait block β€” what a close(2) (or the hub event thread during unplug drain) experiences.

Two full runs on the QEMU guest (DragonFly 6.5-DEVELOPMENT, hz=100):

Phase Scenario run 1 (run.log) run 2 (run.2.log) expected
A open + 1 tcsetattr + close 5078 ms 5090 ms ~5100 ms
2 open + 2 tcsetattrs + close 8141 ms 8094 ms ~8200 ms
B fix #1 (abort on first error) 3149 ms 3121 ms ~3100 ms
C fix #1 + #2 (250 ms bound) 899 ms 925 ms ~850 ms

Math check: 1000+100 (open) + 3Γ—1000+100 (param) + 1000 (close) = 5200 ms total chain; the closer enters 100 ms in β†’ ~5100 ms blocked. Two queued param tasks add one more 3100 ms task β†’ ~8200 ms. Measured values match within tick rounding (hz=100 β‡’ 10 ms granularity).

Conclusion: reproduced. An evil CP210x + a dialer-group local user makes every tcsetattr occupy the adapter's ucom taskqueue ~3.1 s and every close(2)/unplug cycle block uninterruptibly ~5.2 s (8.2 s with two queued tcsetattrs), repeatable indefinitely. Availability-only impact β€” consistent with the family calibration (DF-5367 Low, DF-5383 Low, DF-5421 Low, DF-5424 Low; uslcom's chains are shorter than uchcom's 12-request chain and, unlike uplcom DF-5407, attach does zero EP0 requests so the shared bus explore thread is never pinned at insertion time).

Fix validation

  • fix.diff (sha256 30f138c6659249ca47412f57d5c6e5a80a5b79dc293f78cac3803a17d3073009) is git apply --check-clean against the host sys/ tree and applied cleanly (patch, 9 hunks) to the guest's /usr/src copy.
  • Mechanism #1: uslcom_param() aborts on the first failed request (baud at uslcom.c:525-532 β†’ return; format at 564-570 β†’ return) β€” modeled by harness phase B: 5.1 s β†’ 3.1 s.
  • Mechanism #2: USLCOM_CFG_TIMEOUT 250 ms for all cfg-path requests (open/close/ dtr/rts/param/break; ioctl keeps 1000 ms β€” caller-thread latency only) β€” modeled by phase C: 5.1 s β†’ 0.9 s. This is what also covers the slow-but-successful evil device (answers at 999 ms: no error to abort on, only the bounded timeout helps).
  • The patched production uslcom.c compiles -Werror-clean as the in-tree uslcom.ko kmod on the guest (fix_compile.log, RC=0, uslcom.ko 14440 bytes).
  • Scope caveat: mechanism + compile level, per the family evidence class; an end-to-end run with a physical crafted CP210x is not possible on this guest.

Negative results worth recording (why this file is otherwise clean in pass 2)

  • Line-state RCTRL short-response read (uslcom.c:737-738) β€” usbd_copy_out (usb_busdma.c:248-266) copies a fixed len regardless of actlen, but usbd_callback_wrapper_sub (usb_transfer.c:2979-2992) converts any actlen<sumlen completion of a !short_xfer_ok xfer to USB_ERR_SHORT_XFER before the driver callback runs, and usbd_xfer_state returns ERROR for non-zero error β€” so USB_ST_TRANSFERRED at uslcom.c:736 implies the full 1-byte DATA stage arrived (HCD accounting: xhci.c:713-828 accumulates actual TD bytes into frlengths[]). No stale/uninit byte reaches sc_msr; not a second instance of the DF-1063 class.
  • Baud divisor (uslcom.c:516) β€” guarded: ucom_param() (usb_serial.c:1678-1685) runs uslcom_pre_param (uslcom.c:497-502: 0 < ospeed ≀ 921600) before queueing ucom_cfg_param; no division-by-zero or overflow. The 16-bit USETW truncation for baud < 57 is an upstream-identical functional wart (wrong baud to the device), not a security defect.
  • Watchdog/detach lifecycle (uslcom.c:315-326, 390-404) β€” detach-while-open skips uslcom_close (HL_READY already cleared in ucom_detach_tty:561, taskqueue drained at usb_serial.c:394), leaving the callout armed into usbd_transfer_unsetup; safe by the framework's documented NULL-table protocol (usb_transfer.c:1389-1410: table entry NULLed under the xfer lock, usbd_transfer_start(NULL) returns immediately) and callout_drain's CANCEL semantics (kern_timeout.c:1021-1050: a CANCEL prevents any further callout_reset, so the self-rearming watchdog cannot escape drain before lockuninit).
  • Attach error path (uslcom.c:385-388) β€” calling uslcom_detach on a partially attached device is safe: ucom_detach early-returns unless UCOM_FLAG_ATTACHED (usb_serial.c:371-372), usbd_transfer_setup cleans up its own partial table on failure (usb_transfer.c:1251-1252), and the callout was initialized (uslcom.c:358) so usb_callout_drain is legal.
  • In-flight ioctl vs detach races β€” subsumed by the already-filed framework UAF DF-5320 (usb_serial.c: softc freed while tty/cdev dispatch dangles); not re-reported per pass-2 rules.
  • Bulk callbacks (uslcom.c:664-724) β€” actlen bounded by USLCOM_BULK_BUF_SIZE and HCD DMA accounting; ucom_get_data/ucom_put_data handle the rest. The unclamped-length concern in that path is DF-2959 (usb_serial.c), already filed.

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 on the host repo; patch(1) applied 9/9 hunks cleanly on the guest's /usr/src copy). The patched production driver compiles -Werror-clean as the in-tree uslcom.ko kmod on the guest (fix_compile.log RC=0, uslcom.ko 14440 bytes). Harness phase B models mechanism #1 alone (abort on first error, timeout unchanged): closer block 5.1 s -> 3.1 s; phase C adds mechanism #2 (250 ms cfg timeout bound, which is what covers the slow-but-successful evil device): 5.1 s -> 0.9 s. Scope caveat: mechanism + compile level per the family evidence class; end-to-end with a physical crafted CP210x is not possible on this guest.

fix.diff (sha256 30f138c6659249ca47412f57d5c6e5a80a5b79dc293f78cac3803a17d3073009); fix_compile.log; run.log/run.2.log 'SL5425[B]: closer: unblocked ELAPSED=3149/3121 ms' and 'SL5425[C]: closer: unblocked ELAPSED=899/925 ms' vs 'SL5425[A]: ... 5078/5090 ms'; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5425

Confirmed kernel references

Detail

Exploit chain

evil USB device with one of 80+ uslcom VID:PID pairs enumerates as uslcom; local dialer-group user opens /dev/cuaU* (cfg_open ~1.1 s), then every ioctl(TIOCSETA) queues a 3-request ~3.1 s cfg_param task onto the per-adapter ucom taskqueue; close(2) blocks uninterruptibly behind the queued chain(s) ~5.2 s per cycle (~8.2 s with two queued tsetattrs), repeatable indefinitely; unplugging mid-cycle pins the per-bus USB hub event thread via usb_proc_drain for the in-flight chain.

Evidence (decisive lines)

run.log/run.2.log lines 'SL5425[A]: closer: unblocked ELAPSED=5078/5090 ms', 'SL5425[2]: ... 8141/8094 ms', 'SL5425[B]: ... 3149/3121 ms', 'SL5425[C]: ... 899/925 ms'; VERDICT.md tables the 3+1+1 chain with uslcom.c line cites, traces the close/unplug blocking path, and records the pass-2 negative results (RCTRL short-response proof, divisor guard, watchdog/drain lifecycle); build.log/fix_compile.log show -Werror-clean builds of harness and patched production driver

PoC changes

No seed PoC existed; authored from scratch on the DF-5421/DF-5383 harness pattern with uslcom's production chain shapes (3 reqs/param + hz/10, 1 req/open + hz/10, 1 req/close no pause, 1000 ms) and two extra fix-model phases (B abort-only, C abort+250 ms bound). One bring-up fix: guest file transfer had to switch from python3 (absent) to b64decode; harness compiled and ran correctly on the first build/load.

Verified recommended fix

Abort uslcom_param()'s chain on the first failed vendor request and bound all cfg-path control timeouts (1000 -> 250 ms via USLCOM_CFG_TIMEOUT); full diff in fix.diff

Verdict

Mechanism-level reproduction with production machinery: uslcom_param() (uslcom.c:505-583) chains 3 sequential synchronous EP0 vendor requests per tcsetattr (BAUD_RATE at 520, DATA at 557, SET_FLOWCTRL at 579) and uslcom_open()/uslcom_close() add 1 each (418, 442), all via ucom_cfg_do_request(...,1000) whose usb_error_t every call site discards ('failed (ignored)', uslcom.c:420/522/559/581/444), so no step ever aborts. An evil CP210x (VID/PID-table-only probe, uslcom.c:208-293) that never answers the control data/status stage makes each request eat its full 1000 ms timeout (usb_request.c:642-644, 699-704): ~3.1 s of per-adapter ucom taskqueue occupancy per tcsetattr, ~5.2 s per open+tcsetattr+close cycle. A harness on the production usb_proc_create/msignal/mwait machinery with uslcom's exact request counts shows the closer thread (ucom_close's synchronous usb_proc_mwait, usb_serial.c:671-672+955-957, uninterruptible cv_wait usb_process.c:385-393) blocked 5078/5090 ms behind open+1 tcsetattr and 8141/8094 ms behind open+2 tcsetattrs (local amplification via the sc_param_task double buffer, usb_serial.c:1691-1693); unplug mid-cycle stalls the USB hub event thread identically via ucom_detach's usb_proc_drain (usb_serial.c:394) -- the DF-5367 mechanism, uslcom instance. uslcom-specific amplifier: the open-time watchdog (uslcom.c:315-326) keeps an RCTRL control xfer in flight on the shared ep0 queue, adding up to ~1 s queue-wait per cfg request on top (floor measurements exclude it). fix.diff (param-chain abort on first error + USLCOM_CFG_TIMEOUT 250 ms for all cfg paths) measures 3149/3121 ms (abort) and 899/925 ms (abort+bound) and compiles -Werror-clean as the production in-tree uslcom.ko. No CP210x device model exists on the guest, hence the KLD-harness evidence class; availability-only impact, Low per family calibration (DF-5367/5383/5421/5424).