uchcom cfg callbacks chain 12+2 sequential 1000ms-timeout EP0 vendor requests with no error propagation: evil CH340/CH341 stretches every tcsetattr to ~12s of ucom-taskqueue occupancy, blocking close(2) uninterruptibly and stalling the USB hub event thread on unplug (user-amplifiable)
Summary
cfg_param issues 12 sequential sync EP0 requests per tcsetattr (cfg_open 2 more) via void helpers that DISCARD the usb_error_t - no chain step aborts: a never-answering evil CH341 charges the full 1000ms timeout 12 times = ~12.1s/tcsetattr on the per-adapter ucom taskqueue, with close(2) waiting uninterruptibly (usb_proc_mwait cv_wait) and unplug stalling the hub event thread via usb_proc_drain (the DF-5367 mechanism); the sc_param_task double-buffer lets an unpriv tty holder stack two tasks = ~26s blocks, repeatable forever; a slow-but-successful device isn't even on an error path. Harness-proven with the real usb_proc machinery and uchcom's exact request counts: closer blocked 13888/25777ms baseline, 2101ms with the abort-on-first-error fix (14-hunk, -Werror clean). Availability only.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5421 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uchcom_cfgstall_harness.c | β | 10.4 KB | view raw | |
| Makefile | β | 108 B | β download | |
| build.sh | β | 186 B | view raw | |
| run.sh | β | 151 B | view raw | |
| build.log | β | 7.0 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| run.2.log | β | 1.0 KB | view raw | |
| env.txt | β | 1009 B | view raw | |
| fix_compile.log | β | 1.9 KB | view raw | |
| fix.diff | β | 7.6 KB | view raw | |
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 7.0 KB | β raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw |
DF-5421 β uchcom cfg-chain EP0 stall (evil CH340/CH341)
What (human-readable)
sys/bus/u4b/serial/uchcom.c issues sequential synchronous EP0 vendor control
requests from its ucom cfg callbacks and discards every result:
uchcom_cfg_param()(uchcom.c:652-666) β fired on every tcsetattr of an open tty β chains 12 requests: version read (656β438), RESET write (657), 2 baud writes (658β578-583), LCR read (659β419), LCR write (660), status read (661β488), RESET write (662), 2 baud writes (663), DTR/RTS write (664β453/459), status read (665).uchcom_cfg_open()(uchcom.c:622-631) β every open(2) β chains 2 more.- Every request is
uchcom_ctrl_read/uchcom_ctrl_writeβucom_cfg_do_request(...,1000)(uchcom.c:381-382, 397-398) with a 1000 ms timeout, and both helpers arevoidβ theusb_error_tis thrown away, so no chain step ever aborts.
An evil CH340/CH341 (VID 0x1a86/0x1a2d, uchcom.c:193-196 β probe only checks VID/PID, uchcom.c:293) that simply never answers the control data stage makes each request eat its full timeout (usb_request.c:642-644 "WXFER" sleep, 699-704 timeout) and the chain marches on: ~12.1 s per tcsetattr, ~2.1 s per open. A device that answers just-under-timeout makes every request succeed slowly β same cost, and not even an error to abort on.
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) β DF-5367 mechanism, uchcom instance. - Any local user with the tty open multiplies the occupancy: each tcsetattr re-queues the param task into ucom's double buffer (usb_serial.c:1691-1693) β two outstanding tasks β ~24 s of uninterruptible close(2)/unplug block, repeatable indefinitely.
Reproduce (no CH341 device model exists on the QEMU guest β KLD harness)
# build (in-tree kmod, -Werror clean) mkdir -p /usr/src/sys/bus/u4b/serial/hx5421 cp uchcom_cfgstall_harness.c Makefile /usr/src/sys/bus/u4b/serial/hx5421/ sh build.sh # run sh run.sh # kldload uchcomcfgstall.ko; kldunload; dmesg
Expected (dmesg)
HX5421[A]: closer: unblocked ELAPSED=~13,900 ms (open + 1 tcsetattr + close) HX5421[2]: closer: unblocked ELAPSED=~25,800 ms (open + 2 tcsetattr + close) HX5421[B]: closer: unblocked ELAPSED=~2,100 ms (fix.diff: abort on 1st error)
Measured (3 kldload runs of the final module): A = 13888/13887 ms, A2 = 25772/25767 ms, B = 2101/2132 ms.
Fix
fix.diff β propagate usb_error_t through the vendor-request helpers and
abort each cfg chain on the first failed request (silent device then costs 1
timeout β 1 s per chain, not 12). git apply --check passes against sys/;
the patched driver compiles -Werror-clean as the production uchcom.ko kmod
on the guest (fix_compile.log).
Severity: Low (evil-USB-adapter precondition; family-consistent with DF-5367/DF-5383). Bucket: base:hw.
DF-5421 VERDICT β uchcom cfg-chain synchronous-EP0 stall
Status: reproduced (mechanism-level, KLD harness with production taskqueue machinery β the mandated evidence class for this file, since the QEMU guest has no CH340/CH341 device model). Impact: dos (local, evil-USB-adapter precondition). Confidence: certain.
Root cause (path:line)
uchcom_cfg_param()β sys/bus/u4b/serial/uchcom.c:652-666 β issues 12 sequential synchronous vendor EP0 requests per tcsetattr: | step | call | requests | |---|---|---| | 656 |uchcom_get_version(sc,0)βuchcom_ctrl_read(438) | 1 | | 657 |uchcom_ctrl_write(UCHCOM_REQ_RESET)(381) | 1 | | 658 |uchcom_set_baudrateβ 2 Γuchcom_write_reg(578-583) | 2 | | 659 |uchcom_read_reg(0x18,0x25)(419) | 1 | | 660 |uchcom_write_reg(0x18,0x25)| 1 | | 661 |uchcom_update_statusβ read (488) | 1 | | 662 |uchcom_ctrl_write(UCHCOM_REQ_RESET,0x501f,0xd90a)| 1 | | 663 |uchcom_set_baudrate(again) | 2 | | 664 |uchcom_set_dtr_rts(453 or 459) | 1 | | 665 |uchcom_update_status(again) | 1 |uchcom_cfg_open()β uchcom.c:622-631 β 2 more requests per open(2).- Every request:
ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom, &req, β¦, 1000)β uchcom.c:381-382 (write) and 397-398 (read) β a 1000 ms synchronous control transfer, whoseusb_error_treturn value is discarded (both helpers arevoid, uchcom.c:369-399). No chain step can abort. - Per-request worst case: the device never answers the data stage β the
requester sleeps in the
"WXFER"loop until the timeout fires (sys/bus/u4b/usb_request.c:642-644, 699-704) βUSB_ERR_TIMEOUT, ignored by the driver; next request starts. A device answering just-under-timeout makes every request succeed at ~999 ms (no error path at all) β same cost.
Victim threads (why this is a DoS, not just slowness)
- The chain runs on the per-adapter ucom taskqueue
sc_super_ucom.sc_tq(usb_serial.c:322) undersc->sc_lock. - close(2) blocks uninterruptibly:
ucom_close()βucom_queue_command(sc, ucom_cfg_close, β¦)βusb_proc_mwait()(usb_serial.c:671-672, invoked at 955-957) β uninterruptible cv_wait (usb_process.c:385-393); a message stays head-of-queue while its callback runs (usb_process.c:146-158), so the closer waits out every queued/running cfg task. - Unplug blocks the USB hub event thread:
uchcom_detachβucom_detachβusb_proc_drain(&ssc->sc_tq)(usb_serial.c:394) β DF-5367 mechanism. - Local amplification: every tcsetattr of the open tty re-queues the param
task via ucom's
sc_param_task[2]double buffer (usb_serial.c:1691-1693) β β€2 outstanding tasks β ~24 s block per burst, repeatable indefinitely by an unprivileged user (/dev/ttyU*is 0600 root:wheel but/dev/cuaU*is 0660 uucp:dialer β usb_serial.c:477-488 β and any user who owns an open fd can drive it).
Reproduction (harness = production machinery + production numbers)
uchcom_cfgstall_harness.c uses the real usb_proc_create/msignal/mwait/
free machinery; each request is modeled at its exact production cost
(lksleep("WXFER", 1000ms) β what usbd_do_request_flags charges for a
never-answered control transfer); request counts 12 (param task) and 2 (open
task) are uchcom's; the closer thread performs exactly ucom_close's
synchronous usb_proc_mwait.
Phases (all at kldload, output via dmesg):
| phase | shape | expected | measured (final module) |
|---|---|---|---|
| A | open(2) + 1 tcsetattr + close(2) | ~14.1 s | 13888 / 13887 ms |
| A2 | open(2) + 2 tcsetattr + close(2) | ~26.2 s | 25772 / 25767 ms |
| B | fix.diff mechanism (abort chain on 1st failed request) | ~2.1 s | 2101 / 2132 ms |
(The measured values sit ~2 % under the arithmetic sum because hz=100 tick
rounding shaves each sleep slightly; run.log/run.2.log contain the full dmesg.)
An earlier harness iteration modeled the fixed chain's aborting request as
instant (B=137 ms); that overstated the fix β corrected to charge the one real
timeout before the abort (Bβ2.1 s). Iterations documented; attempts=3.
Fix validation
fix.diff(sha256 5f7d53dcβ¦) βusb_error_tpropagation throughuchcom_ctrl_write/ctrl_read/write_reg/read_reg/get_version/get_status/ set_dtr_rts_10/_20/update_version/update_status/set_dtr_rts/set_baudrate, abortuchcom_cfg_paramanduchcom_cfg_openon the first failed request, and skipuchcom_cfg_set_break's read-modify-write when the read fails.git apply --checkpasses against the hostsys/tree.- Patched driver (guest md5 cc08d58eβ¦) compiles -Werror-clean as the
production
uchcom.kokmod in-tree on the guest (fix_compile.log, RC=0); pristine file restored and md5-verified afterwards (3616dd7eβ¦). - Mechanism measured in harness phase B: 25.8 s-class block β 2.1 s-class. Scope caveat: mechanism + compile level; end-to-end with a physical crafted CH341 is not possible on this guest (no device model). A complementary hardening (not in fix.diff): bound the cfg-path timeout (1000 β 100-250 ms) to also cover slow-but-successful evil devices.
Why the other audited classes are NOT findings here (pass-2 negative notes)
- Divisor math (uchcom.c:530-568):
rate==0(B0 hangup) cannot reach the divisions β the range table (183-191, alldvr_low β₯ 1) rejects it at 539-546 βpre_paramreturns EIO (645-647); no rate in [1, 2999999] can overflowrem<<1/modin uint32. Contrast umcs (DF-1048/DF-2343). - Bulk/intr callbacks (uchcom.c:712-816): intr copies exactly 4 bytes under
an
actlen >= 4check (726-728); read path guardsactlen > 0and is bounded by the 1024-byte xfer bufsize (797-799); write path takesucom_get_data's bounded fill (765-766). No device-fed size reaches a copy unbounded. - Uninit via control-IN: error-path and short-xfer-tail handling is the
known class (DF-1062/DF-2342) β
usbd_do_request_proczeroes the buffer on error (usb_request.c:783-785) butucom_cfg_do_requestpassesactlen=NULL(usb_serial.h:220-221), so successful short reads leave the tail β already filed, not re-reported. - Attach/detach error paths (uchcom.c:296-363):
usbd_transfer_setupfailure NULLs and self-unsets (usb_transfer.c:935, 1251-1252);ucom_detachearly-returns when never attached (usb_serial.c:371-372);lockuninitafterusbd_transfer_unsetupis safe (callbacks complete before it returns, usb_transfer.c:1362-1364). Clean. ucom_poll(uchcom.c:818-823): registered but the core never calls it (console path#if 0, usb_serial.c:520) β dead.- Core-file issues (DF-2959 TX clist, DF-5320 unplug tty UAF) belong to usb_serial.c and are not re-reported here.
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, host repo); the patched production driver compiles -Werror-clean as the in-tree uchcom.ko kmod on the guest (fix_compile.log RC=0, uchcom.ko produced); harness phase B with the abort-on-first-error mechanism measures the closer block at 2101/2132 ms vs 13888/25772 ms baseline (12.1 s per chain -> ~1.1 s per chain against a silent device). Scope caveat: mechanism + compile level; end-to-end with a physical crafted CH341 is not possible on this guest (no device model); the slow-but-successful-device variant additionally needs the timeout bound noted in recommended_fix.
fix.diff (sha256 5f7d53dc6a251fa0276a8253b84df6d1aff7f13f9d484a1ec523a16efee7ec83); fix_compile.log; run.log/run.2.log 'HX5421[B]: closer: unblocked ELAPSED=2101/2132 ms'; VERDICT.md 'Fix validation' section
Confirmed kernel references
- sys/bus/u4b/serial/uchcom.c:652-666
- sys/bus/u4b/serial/uchcom.c:622-631
- sys/bus/u4b/serial/uchcom.c:369-399
- sys/bus/u4b/serial/uchcom.c:578-584
- sys/bus/u4b/serial/usb_serial.c:671-672
- 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 WCH CH340/CH341 VID/PID enumerates as uchcom, then never answers vendor control data stages -> open(/dev/cuaU*) queues cfg_open (~2.1 s); every tcsetattr costs ~12.1 s of sc_tq occupancy; close(2) blocks uninterruptibly behind queued tasks (~14 s single cycle, ~26 s with two queued tsetattrs); unplug during a cycle pins the per-bus USB hub event thread via usb_proc_drain for the in-flight chain (~12 s); an unprivileged user with the tty open repeats indefinitely. Availability-only impact.
Evidence (decisive lines)
run.log/run.2.log lines 'HX5421[A]: closer: unblocked ELAPSED=13888/13887 ms', 'HX5421[2]: ... 25772/25767 ms', 'HX5421[B]: ... 2101/2132 ms'; VERDICT.md tables all 12+2 requests with uchcom.c line cites and traces the close/unplug blocking path; 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-5367/DF-5383 harness pattern. Two fixes during bring-up: (1) -Werror unused-function -> wired cfg_open into every phase (production open->tcsetattr->close shape, which strengthened the demo); (2) phase B initially modeled the aborting request as instant (137 ms) which overstated the fix -> corrected to charge the one real 1000 ms timeout before abort (B~2.1 s).
Verified recommended fix
Propagate usb_error_t through the uchcom vendor-request helpers and abort cfg_param/cfg_open on the first failed request (full diff in fix.diff); optionally also bound the cfg-path control timeout (1000 -> 100-250 ms) to cover slow-but-successful evil devices
Verdict
Mechanism-level reproduction with production machinery: uchcom_cfg_param() (uchcom.c:652-666) chains 12 sequential synchronous EP0 vendor requests per tcsetattr and uchcom_cfg_open() (uchcom.c:622-631) 2 more per open(2), each via void helpers that discard the usb_error_t of a 1000 ms-timeout control transfer (uchcom.c:369-399, 381-382, 397-398), so no step ever aborts. An evil CH340/CH341 (VID/PID-only probe, uchcom.c:193-196, 293) that never answers the control data stage makes each request eat its full timeout (usb_request.c:642-644, 699-704): ~12.1 s occupancy of the per-adapter ucom taskqueue per tcsetattr. A harness on the production usb_proc_create/msignal/mwait machinery, with uchcom's exact request counts and per-request cost, 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 13888/13887 ms behind open+1 tcsetattr and 25772/25767 ms behind open+2 tcsetattr (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, uchcom instance. fix.diff (error propagation + chain abort) measures 2101/2132 ms and compiles -Werror-clean as the production uchcom.ko. No CH341 device model exists on the guest, hence the KLD-harness evidence class; no memory corruption (ucom_pre_param validates the rate before cfg_param runs, so the divisor math is never fed garbage).
No comments yet.