# DF-5604 — post-unlock use of shared per-udev scratch in `device_set_usb_desc`

**File:** `sys/bus/u4b/usb_util.c` (pass-2 GLM 5.3 re-audit)
**Severity:** Low · **Class:** CWE-367 (TOCTOU) / CWE-125 (OOB read)
**Verdict:** reproduced (in-kernel mechanism, deterministic 5/5) — see `verdict.json`

## What the finding is

`device_set_usb_desc()` (sys/bus/u4b/usb_util.c:94-115) fills the shared
per-device scratch buffer `udev->scratch.data` (255 bytes, shared by *every*
enumeration path of the udev and documented as "protected by the enumeration
lock", sys/bus/u4b/usb_device.h:168-170) under `usbd_enum_lock()`, then
**drops the lock** (line 110-111) and only afterwards hands the scratch
pointer to:

* `device_set_desc_copy(dev, temp_p)` → `device_set_desc_internal()` →
  unbounded `strlen(desc)` + `strcpy()` (sys/kern/subr_bus.c:1759-1775), and
* `device_printf(dev, "<%s>", temp_p)` (usb_util.c:114) — printing whatever
  the (possibly concurrently rewritten) scratch contains to console/msgbuf.

A concurrent enumeration operation on the same udev that takes the enum lock
in that window and calls `usbd_req_get_string_any()` (usb_request.c:1095)
first DMAs a **device-controlled raw descriptor copy** over the scratch
(clobbering the string *and its NUL terminator*) and only terminates the
converted string at the very end (`*s = 0`, usb_request.c:1176). A malicious
device that supplies 16-bit string characters (every raw byte nonzero, every
char "silently skipped" by the filter) leaves the whole 255-byte scratch
NUL-free for the entire fetch+conversion window — the reader's `strlen()`
then walks out of `scratch.data` into the rest of `struct usb_device` and
the adjacent kernel heap, and those bytes are copied into `dev->desc` and
printed to the console (`dmesg`-visible kernel-heap disclosure; panic if the
scan crosses into an unmapped page).

FreeBSD's current `device_set_usb_desc` (cgit, sys/dev/usb/usb_util.c) no
longer fetches device strings into scratch here at all (it uses the ctrl
lock and only formats cached `udev->manufacturer/product` strings) —
DragonFly's variant is the stale pre-refactor shape with a strictly wider
window (raw device-controlled bytes in the shared buffer).

## Reproduce

Build + run on the audit QEMU guest (see `../` for guest lifecycle):

```sh
./build.sh      # cc via bsd.kmod.mk → usbutil_probe.ko (-Werror clean)
./run.sh        # kldload; runs case 0 (matrix) at load;
                # sysctl hw.usutil_probe=1  → control + raced runs
                # sysctl hw.usutil_probe=2  → deliberate NULL panic (LAST)
```

Expected decisive output (see `run.log`, `run.2.log`, `panic.txt`):

```
UPROBE case0 matrix: 108 cells, 0 failures (usb_make_str_desc write discipline)
UPROBE case1(control): strlen(scratch)=25 scratch=255 bytes_past_boundary=0 ...
UPROBE case1(RACED): strlen(scratch)=1023 scratch=255 bytes_past_boundary=769
    desc[255..290]="ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
```

* `case0` is the **negative evidence** for the checklist item "check
  bLength/truncation discipline hard": exhaustive boundary matrix
  (max_len ∈ {0,1,2,3,4,5,254,255,256,511,512,513,1023,1024,32767,32768,
  65534,65535} × strings {"", 1, 31, 126, 127, 1000 chars}) with canary
  checks — `usb_make_str_desc` never writes at or past `totlen`, never past
  `max_len` (odd tail byte included), bLength is always exact, UTF-16LE
  payload always correct. 108/108 PASS.
* `case1` reproduces the DF-5604 race mechanism with the verbatim code
  shapes of the production writer (`usbd_req_get_string_any`,
  usb_request.c:1129-1176), the production lock discipline
  (`usbd_enum_lock`/`unlock`, usb_util.c:94-111) and the production reader
  (`strlen`+copy as in subr_bus.c:1759-1775). Control run: 25 bytes.
  Raced run: `strlen` runs 769 bytes past the 255-byte scratch boundary and
  captures adjacent-heap marker bytes (`0x5A`) into the would-be
  `dev->desc` / `<%s>` console string. Deterministic, 5/5 runs.
* `case2` documents the missing `s == NULL` guard in `usb_make_str_desc`
  (usb_util.c:228 `strlen(s)`): a NULL string panics the kernel at
  `strlen: cmpb $0,(%rdi)` (fault address 0x0). No production caller passes
  NULL today (all four callers pass constants/`sc->vendor`), so this is a
  hardening note, not a second finding.

## Scope of the reproduction (read this)

The harness drives the production **code shapes** with adversarial
scheduling inside a KLD; it does not call `device_set_usb_desc()` itself,
because that requires a live enumerated USB device and a real concurrent
same-udev enumeration thread (the natural sources are hub-explore racing a
devd/kldload-triggered re-probe). The defect, the window and the OOB read
are fully code-proven (VERDICT.md) and the mechanism is demonstrated
deterministically in-kernel. The production interleave is not driven
end-to-end — that is why severity stays Low and confidence `likely`.

## Fix

`fix.diff`: snapshot the scratch string into a 256-byte stack buffer *while
the enum lock is still held*, then unlock and use the snapshot.
