# VERDICT — DF-2959

**Status: untested** (out of Phase-V scope: Low severity, latent reachability,
and the audit QEMU guest has no USB serial hardware). This pack documents a
static-analysis-confirmed defect found during the pass-2 consumer sweep of
`sys/kern/tty_subr.c` (the clist API): a caller passes an unclamped length
to `clist_qtob`, whose documented contract (tty_subr.c:127-149) trusts `n`
as the destination bound.

## Why this is a real defect (code-level certainty)

1. `sys/bus/u4b/serial/usb_serial.c:1898` obtains the contiguous DMA segment
   `(res.buffer, res.length)` via `usbd_get_page`.
2. `usb_serial.c:1900-1903` clamps `res.length` to the request — proving the
   author's intent that at most `res.length` bytes may touch `res.buffer`.
3. `usb_serial.c:1906` then ignores the clamp and calls
   `clist_qtob(&tp->t_outq, res.buffer, len)` with the full request length
   `len` (e.g. `UPLCOM_BULK_BUF_SIZE`, uplcom.c:838).
4. `clist_qtob` writes exactly `min(len, t_outq.c_cc)` bytes to `res.buffer`
   (tty_subr.c:138-144) — the only bound it enforces is the queue count.
5. `usbd_get_page` (usb_busdma.c:96-108) yields
   `res.length = USB_PAGE_SIZE - (offset % USB_PAGE_SIZE)` for multi-segment
   caches (as low as 1), and for Case 1b returns `res.buffer = page->buffer +
   offset`, a standalone per-page allocation — so a segment overrun is a heap
   OOB write into foreign memory, not merely into slack of one big mapping.
6. The sibling RX path in the same function family
   (`ucom_put_data`, usb_serial.c:2004-2032) uses `res.length` correctly
   (`buf = res.buffer; cnt = res.length; … clist_btoq((char *)buf, cnt, …)`),
   so the TX line is an asymmetry/bug, not a subtlety.

Overflow preconditions: multi-segment (or non-KVA) TX page cache AND
`t_outq.c_cc > res.length`. Both are satisfiable by the API contract but are
not produced by the stock serial drivers on x86 (single-segment ≤1-page bulk
buffers → `res.length = (usb_size_t)-1` → clamp == `len`). Hence "latent".

## Impact ceiling (if preconditions met)

Kernel heap out-of-bounds write: contents = bytes the user previously
`write()`d to the /dev/ttyU* device (fully attacker-chosen), length =
`min(len, queued bytes) - res.length` (up to ~1KB), repeatable per TX
callback. That is a classic heap-corruption primitive → potential local
privilege escalation on machines with USB serial adapters; not demonstrated
here because no such hardware exists on the guest.

## Why not reproduced on the guest

* `vm.sh run_user 'ls /dev/ttyU*'` → `No such file or directory`;
  `ucom.ko`, `uplcom.ko`, `uftdi.ko`, `u3g.ko` exist in /boot but there is
  no USB device for the drivers to bind, so `ucom_get_data` never executes.
* Severity Low (hardware-gated, masked in common config) → per contract,
  Phase V is conditional and was not warranted for this finding.

## Negative results recorded during the same pass-2 sweep

(everything hunted in tty_subr.c itself and its consumers, all killed)

* Ring arithmetic `clist_getc/putc/btoq/qtob/ndflush/unputc`: single
  subtraction `i -= c_ccmax` is sufficient under the invariants
  `0 <= c_cchead < c_ccmax`, `0 <= c_cc <= c_ccmax`, which every function
  preserves for non-negative inputs — tty_subr.c:113-116,140-141,161-163,
  179-181,205-207,282-284.
* `clist_alloc_cblocks` shrink/grow copy math (tty_subr.c:64-76): proven
  in-bounds (`count <= ccmax`, `n <= c_ccmax-c_cchead`, `count-n <= c_cchead`).
* Blocking-`kmalloc`(M_INTWAIT) TOCTOU (tty_subr.c:61-80): killed — LWKT
  tokens are re-acquired before the blocked thread resumes, so the
  post-kmalloc field reads (lines 64-80) run under the caller's tty token;
  the "NOTE: cl fields may now be different" comment is handled by the
  fresh re-read.
* sio interrupt vs tty-softcall concurrency: killed — sio touches clists
  only via `comstart`/`comparam`/`comstop`, each reached with `tp->t_token`
  held (`ttstart` tty.c:1549-1556, `ttyinput` tty.c:404, `ttyflush`
  tty.c:1433, `ttioctl` tty.c:1056); `sioinput`→`ttyinput` takes the token
  itself (tty.c:404).
* Huge `clist_alloc_cblocks` size via user `termios.c_ispeed`
  (tty.c:1082 → ttsetwater:2439,2456): killed for reachable devices — sio's
  `comparam` rejects `c_ispeed != c_ospeed` and invalid divisors
  (sio.c:2275-2283); ptys have `t_ispeedwat == 0` → clamped legacy path
  (tty.c:2444-2449).
* Quoted-character sentinel collision (`clist_getc` returning
  TTY_QUOTE-flagged shorts): killed — `TTY_QUOTE == 0x0100`
  (sys/sys/tty.h:208), stored values ≤ 0x1ff are always positive as int, so
  `>= 0` loops (e.g. ttypend, tty.c:1665) and `!= -1` loops are equivalent.
* `ccmax * sizeof(short)` signed overflow for `ccmax ∈ (2^30, 2^31)`:
  same unvalidated-`ccmax` root cause and same fix as known DF-0211 —
  folded into that finding's remediation, not re-filed.
* Info leak from `clist_qtob`/`clist_getc`: killed — every readable element
  is written by putc/btoq/alloc-copy; new buffers are `M_ZERO`
  (tty_subr.c:61).
