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)
PoC verification
Evidence pack
findings/poc/DF-5425 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_RATEdivisor write (uslcom.c:514-523, request issued at 520),USLCOM_DATAformat write (uslcom.c:551-560, at 557),USLCOM_SET_FLOWCTRL16-byte out (uslcom.c:573-582, at 579).uslcom_open()(uslcom.c:406-425) β every open(2) β adds 1 (USLCOM_UARTenable, at 418).uslcom_close()(uslcom.c:427-446) β every close(2) β adds 1 (USLCOM_UARTdisable, at 442).- Every request is
ucom_cfg_do_request(..., 1000)with a 1000 ms timeout, and every call site throws theusb_error_taway β "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) holdingsc->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).
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()queuesucom_cfg_closethenusb_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 thesc_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) isgit apply --check-clean against the hostsys/tree and applied cleanly (patch, 9 hunks) to the guest's/usr/srccopy.- 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_TIMEOUT250 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.ccompiles-Werror-clean as the in-treeuslcom.kokmod 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 fixedlenregardless of actlen, butusbd_callback_wrapper_sub(usb_transfer.c:2979-2992) converts any actlen<sumlen completion of a!short_xfer_okxfer toUSB_ERR_SHORT_XFERbefore the driver callback runs, andusbd_xfer_statereturns ERROR for non-zero error β soUSB_ST_TRANSFERREDat 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 reachessc_msr; not a second instance of the DF-1063 class. - Baud divisor (uslcom.c:516) β guarded:
ucom_param()(usb_serial.c:1678-1685) runsuslcom_pre_param(uslcom.c:497-502: 0 < ospeed β€ 921600) before queueingucom_cfg_param; no division-by-zero or overflow. The 16-bitUSETWtruncation 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 intousbd_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) andcallout_drain's CANCEL semantics (kern_timeout.c:1021-1050: a CANCEL prevents any furthercallout_reset, so the self-rearming watchdog cannot escape drain beforelockuninit). - Attach error path (uslcom.c:385-388) β calling
uslcom_detachon a partially attached device is safe:ucom_detachearly-returns unlessUCOM_FLAG_ATTACHED(usb_serial.c:371-372),usbd_transfer_setupcleans up its own partial table on failure (usb_transfer.c:1251-1252), and the callout was initialized (uslcom.c:358) sousb_callout_drainis 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_SIZEand HCD DMA accounting;ucom_get_data/ucom_put_datahandle 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
fixedfix.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
Confirmed kernel references
- sys/bus/u4b/serial/uslcom.c:505-583
- sys/bus/u4b/serial/uslcom.c:514-523
- sys/bus/u4b/serial/uslcom.c:551-560
- sys/bus/u4b/serial/uslcom.c:573-582
- sys/bus/u4b/serial/uslcom.c:418-421
- sys/bus/u4b/serial/uslcom.c:442-445
- sys/bus/u4b/serial/uslcom.c:315-326
- sys/bus/u4b/serial/usb_serial.c:671-672
- sys/bus/u4b/serial/usb_serial.c:955-957
- sys/bus/u4b/serial/usb_serial.c:1691-1693
- sys/bus/u4b/serial/usb_serial.c:394
- sys/bus/u4b/usb_request.c:642-644
- sys/bus/u4b/usb_request.c:699-704
- sys/bus/u4b/usb_process.c:385-393
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).
No comments yet.