# DF-5578 — ufm(4): `ufm_do_req()` reads uninitialized stack `buf[0]` into
# `*retbuf` before the error check — latent DF-1062-class disclosure,
# CONTAINED on the current stack (Info)

Production file: `sys/bus/u4b/misc/ufm.c` (Cypress CY7C68xxx FM radio,
`/dev/ufm*`). Evidence class: **KLD-harness-with-production-code +
synthetic usbd request capture** (family standard, DF-5455): no Cypress
0x04b4:0x1002 radio exists on the QEMU guest, so the verbatim production
driver is compiled into a KLD and its EP0 transport is rerouted to a
capture shim reproducing the traced `usbd_do_request_flags()` completion
semantics.

## What the code does

`ufm_do_req()` (ufm.c:176-200):

```c
uint8_t buf[1];                              /* 183: UNINITIALIZED      */
USETW(req.wLength, 1);                       /* 189                     */
error = usbd_do_request(sc->sc_udev, NULL, &req, buf);   /* 191        */
if (retbuf) { *retbuf = buf[0]; }            /* 193-194: BEFORE the
                                                error check            */
if (error) { return (ENXIO); }               /* 196-198                 */
```

`buf[0]` is loaded and stored to `*retbuf` **even when the request
failed** — on every timeout/short/stall the caller's byte receives stale
kernel stack. Two callers then matter: `ufm_get_stat()` copies `ret`
into the `FM_GET_STAT` ioctl word (ufm.c:292 → copyout at
sys_generic.c:730), and `ufm_start()` branches on it (ufm.c:257).

## Why it is contained today (and the one-flag counterfactual)

ufm uses the **plain** `usbd_do_request` macro = `usbd_do_request_flags(
udev, mtx, req, data, 0 /* flags */, NULL /* actlen */, …)`
(usbdi.h:511-512). With flags=0 and the default EP0 template carrying no
short-ok flags (usb_transfer.c:76-88), a short control-IN DATA stage
completes with `USB_ERR_SHORT_XFER` (usb_transfer.c:2979-2985) — an
**error** — so `ufm_do_req` returns ENXIO and both callers discard `ret`
(ufm.c:253-256, 288-291). err==0 without the 1-byte copy-out is provably
impossible: success requires `actlen == sumlen` (usb_transfer.c:2952+),
`actlen` sums completed frames (usb_transfer.c:2936-2940), so a full
1-byte DATA stage was DMA'd and copied (usb_request.c:680-681).

The counterfactual (harness mode 3) is exactly the DF-1062 delta: with
`USB_SHORT_XFER_OK` (uchcom.c:398) or via `usbd_do_request_proc` with
`actlen == NULL` (usb_serial.h:220 — the stack's zero-pad-on-short only
exists behind a non-NULL actlen, usb_request.c:782-787), the same call
site returns err==0 with nothing copied and `FM_GET_STAT` hands one
stale kernel-stack byte to the ioctl caller. The finding is filed so a
one-flag drift (or a caller that starts logging `ret` on error) does not
silently turn ufm into DF-1062/#5455.

Exposure ceiling even in the counterfactual: `/dev/ufm*` is created
0644 root:operator (ufm.c:153) and `ufm_ioctl` requires O_RDWR
(ufm.c:304-306), so the only default opener is root — matching the
Info severity.

## Build

```
sh build.sh    # in-tree kmod build on the guest:
               # /usr/src/sys/bus/u4b/misc/df5578 + make
               # (guarded copy ufm_prod_guarded.c = production ufm.c
               #  sha256 845738f2... + ONE guard hunk, prod_guard.diff)
```

## Run

```
sh run.sh      # kldload ufmunleak.ko x3, kldunload, dmesg | grep UFM5578
```

## Expected (decisive) output

```
UFM5578: gate fflags=FREAD        : rc=13 (EACCES) out=0x12345678
UFM5578: FULL FM_GET_STAT short   : rc=5 (EIO) out=0x000000cc -- LEAK BLOCKED (discarded)
UFM5578: FULL FM_GET_STAT silent  : rc=5 (EIO) out=0x000000cc -- LEAK BLOCKED (discarded)
UFM5578: FULL FM_GET_STAT full    : rc=0 out=0x0000005c -- device byte delivered
UFM5578: FULL FM_GET_STAT shortok : rc=0 out=0x.. -- STALE STACK BYTE REACHES IOCTL CALLER
UFM5578: calibrate: poison depth 1 MATCHES buf[0] slot
UFM5578: pat=0xff rep=0 mode=short-ok   : PROD ret=0xff MATCH          (18/18 total)
UFM5578: pat=0xff rep=0 FIXED(buf={0}) : ret=0x00 (clean)              (18/18 total)
UFM5578: RESULT: counterfactual short-ok: 18/18 pattern-match; fixed twin clean 18/18
```

plus the request capture (EP0 composition audit):

```
SET_FREQ 100700000 Hz -> SET_FREQ bReq=1 wVal=0x0022 wIdx=0x22d0 ; CMD0 bReq=0 wVal=0x0096 wIdx=0x00b7 (bm=0xc0 wLen=1)
SET_FREQ 2147483647 Hz -> ... wVal=0xfd64 wIdx=0x6442   (signed-overflow wrap, device-bound only)
FM_START rc=0: [0] bReq=0 0x0000/0x00c7  [1] bReq=2 0x0001/0x0000
FM_STOP  rc=0: [0] bReq=0 0x0016/0x001c  [1] bReq=2 0x0000/0x0000
```

## Fix

`fix.diff` — initialize the buffer (`uint8_t buf[1] = {0};`, one line,
same shape as the DF-5455 umoscom fix). Validated: in-harness fixed twin
under identical poisoning returns 0x00 in every mode (18/18), and the
patched production `ufm.ko` builds `-Werror`-clean (`fix_build.log`,
rc=0).
