# DF-5542 VERDICT — permanent input stall in uep(4) reassembly (sc->buf_len never cleared on bad-header path)

**Status: reproduced** (deterministic, 2/2 identical runs; fix validated).
**Impact: dos** — permanent loss of the touch-panel input stream (device
remains polled but every byte is dropped) until unplug/replug. No memory
unsafety involved.

## Root cause (path:line)

`uep_intr_callback()` reassembly continuation path, stock
`sys/bus/u4b/input/uep.c`:

```c
216:  if (sc->buf_len > 0) {
217:      int res;
218:
219:      if (sc->buf_len == 1)
220:          sc->buf[1] = buf[0];
221:
222:      if ((pkt_len = get_pkt_len(sc->buf)) == 0)
223:          goto tr_setup;              /* <-- sc->buf_len stays > 0 forever */
```

and the unvalidated single-byte stash that arms it:

```c
235:  if (len == 1) {
236:      sc->buf[0] = buf[0];           /* header byte NOT validated */
237:      sc->buf_len = 1;
238:      goto tr_setup;
239:  }
```

`get_pkt_len()` (uep.c:121-147) returns 0 for any first byte with
`buf[0] != 0x0a` (DIAG) and `buf[0] & 0xe0` not in {0x80, 0xa0, 0xc0,
0xe0} — i.e. 96 of 255 possible bytes (0x00–0x7f minus 0x0a).

Attack sequence (all bytes device-supplied; interrupt transfers are
`short_xfer_ok`, uep.c:283, so the device chooses each transfer's length):

1. Device delivers a 1-byte interrupt transfer `00`. uep.c:235-239 stashes
   it (`sc->buf[0]=0x00`, `sc->buf_len=1`) without parsing it.
2. Every subsequent transfer takes the continuation path (216). For a
   non-DIAG stash byte, `get_pkt_len()` reads only `sc->buf[0]` and returns
   0 -> `goto tr_setup` (222-223) **without clearing `sc->buf_len`**. The
   entire new transfer is discarded (it lives in the stack local `buf`).
3. Goto 2, forever. `submits=11` in the run log proves the xfer keeps
   being resubmitted (uep.c:261-264) while `queued_reports=0` proves no
   data ever reaches the FIFO. The only code that ever writes 0 to
   `sc->buf_len` after this point is uep.c:338 (attach) — so only
   unplug/replug recovers. `uep_close()` (uep.c:413-422) does not reset the
   reassembly state either, so close(2)/open(2) of /dev/uep0 does not clear
   the wedge (and stale fragment bytes bleed across sessions).

Contrast with the Linux eGalax driver this code mimics, which resets the
fragment length before dropping a bad header (usbtouchscreen.c,
egalax process path), and with FreeBSD uep.c, which carries the same two
defects (imported code) — the fix applies upstream verbatim.

## Threat model

- Malicious/reprogrammed USB device presenting an eGalax VID/PID (0x3823/
  0x0001, 0x3823/0x0002, 0x0123/0x0001 — uep.c:289-293): a single 1-byte
  transfer kills the input stream permanently.
- Flaky cable/EMI garble on a legitimate panel: same effect from noise.
- Consequence: touches stop being reported (a "frozen" touchscreen while
  the device node stays open and blocks in read(2)); availability only —
  no confidentiality or integrity impact, no memory unsafety.

## Evidence

- Harness: production `uep.c` (sha256-verified byte-for-byte, env.txt)
  compiled into `ueph.ko`; synthetic TRANSFERRED callbacks into the real
  `uep_intr_callback`; shadow FIFO records the exact
  `usb_fifo_put_data_linear()` records that `/dev/uep0` readers would
  receive. Deviation from the family's "cloned fd" style: uep has no
  QEMU-emulatable hardware (unlike ums/ukbd), and the finding lives
  entirely in the parser state machine, for which the shadow FIFO is a
  faithful record of the only producer->FIFO call site (uep.c:391-392).
- `run.log` / `run.2.log`: T2 `queued_reports=0 ... buf_len=1 ...
  verdict=PERMANENT_STALL` (stock, deterministic across runs); T1 control
  proves the parser is healthy on well-formed input; T4 shows sibling
  DF-5543 arming this same wedge.

## Fix validation

`fix.diff` hunk 1 adds `sc->buf_len = 0;` on the bad-header continuation
path (drop stale fragment, resync next transfer); hunk 2 (DF-5543) makes
the stash store the remaining stream byte `p[0]`; hunk 3 resets `buf_len`
in `uep_close()` so no fragment state crosses sessions.

Patched run (`fix_run.log`): T2 `queued_reports=27 buf_len=0
verdict=no_stall` — the first post-fragment transfer is dropped (resync),
then 9x3 valid reports flow; T1 unchanged; T4 `no_stall` (13 reports).
The patched production file also builds as the real `uep.ko` module with
`-Werror` (`fix_build.log`). Guest `/usr/src` restored to stock afterwards
(sha256 match in env.txt); harness module unloaded.

## Bottom line

Deterministic, hardware-free reproduction against the unmodified
production parser; availability-only impact (device-input DoS); two-line
core fix validated on the production file in both harness and real-module
builds.

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