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