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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5421 Β· 14 files
FileTypeDescriptionSize
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 are void β€” the usb_error_t is 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) 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) β€” 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.

VERDICT.md
↓ download raw

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, whose usb_error_t return value is discarded (both helpers are void, 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) under sc->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_t propagation through uchcom_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, abort uchcom_cfg_param and uchcom_cfg_open on the first failed request, and skip uchcom_cfg_set_break's read-modify-write when the read fails.
  • git apply --check passes against the host sys/ tree.
  • Patched driver (guest md5 cc08d58e…) compiles -Werror-clean as the production uchcom.ko kmod 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, all dvr_low β‰₯ 1) rejects it at 539-546 β†’ pre_param returns EIO (645-647); no rate in [1, 2999999] can overflow rem<<1/mod in uint32. Contrast umcs (DF-1048/DF-2343).
  • Bulk/intr callbacks (uchcom.c:712-816): intr copies exactly 4 bytes under an actlen >= 4 check (726-728); read path guards actlen > 0 and is bounded by the 1024-byte xfer bufsize (797-799); write path takes ucom_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_proc zeroes the buffer on error (usb_request.c:783-785) but ucom_cfg_do_request passes actlen=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_setup failure NULLs and self-unsets (usb_transfer.c:935, 1251-1252); ucom_detach early-returns when never attached (usb_serial.c:371-372); lockuninit after usbd_transfer_unsetup is 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

fixed
baseline reproduced→ patch + rebuild →patched reproduced

fix.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
↓ fix.diffper-fix-DF-5421

Confirmed kernel references

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).