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