# DF-5664 VERDICT — usb_alloc_mbufs() 32-bit alloc_size truncation

## Status: REPRODUCED (privileged KLD harness) — latent, no unprivileged in-tree path

## The bug

`sys/bus/u4b/usb_mbuf.c:62` declares `usb_size_t alloc_size` (uint32_t per
`sys/bus/u4b/usb_dragonfly.h:92`). Line 69:

```c
alloc_size = (block_size + sizeof(struct usb_mbuf)) * nblocks;
```

`block_size` is `usb_size_t` (u32), `sizeof()` is `size_t` (u64 on LP64), and
`nblocks` is `uint16_t` — usual arithmetic conversions evaluate the RHS in
**64-bit**, and the assignment then **truncates to 32 bits**. `kmalloc()`
(usb_mbuf.c:71) receives only the truncated count, while the population loop
(usb_mbuf.c:77-91) lays out `nblocks` headers followed by data areas at the
full, untruncated stride:

```c
data_ptr = (void *)(m_ptr + nblocks);      /* headers first            */
...
data_ptr += block_size;                    /* full stride per block    */
```

Any product ≥ 2^32 therefore yields an undersized allocation whose data
pointers (and, for larger nblocks, even the header array) reach past its end.
Because `block_size` is always 8-aligned and `sizeof(struct usb_mbuf)` is 48
(multiple of 8), a caller can choose `nblocks = 2^32 / block_size` exactly,
making the truncated size land precisely at `nblocks*48` — headers fit, and
the first data pointer sits at exactly one-past-the-end of the chunk
(quiet adjacent-slab corruption), with later data pointers up to `nblocks *
block_size` bytes beyond (wild writes). Full attacker control of both
factors is required; today only in-kernel drivers call this API.

## Reachability analysis (why this is Low/hardening, not High)

The only in-tree caller is `usb_fifo_alloc_buffer()` (usb_dev.c:1984,
called from usb_dev.c:1993). All its call sites and their ceilings:

| Caller | block_size | nblocks | product |
|---|---|---|---|
| ugen bulk/interrupt read/write (usb_generic.c:156-157) | `max_data_length` ≤ 256 KiB + 48 (`ugen_set_buffer_size` cap, usb_generic.c:1669-1672) | 2 | ~512 KiB |
| ugen ISO (usb_generic.c:159-161) | `max_frame_size` ≤ 1024×48 = 49152 (xHCI `hc_max_packet_count=16*3`, xhci.c:3767; std size filter usb_transfer.c:477) | 2×nframes ≤ 2×960 (`USB_MAX_HS_ISOC_FRAMES_PER_XFER=8*120`) | ≈ 94 MB |
| USB_FS_INIT (usb_generic.c:2280) | 1 | ≤ 127 | ≈ 7 KB |
| uhid (uhid.c:519,525) | `sc_isize+1` ≤ 64K-ish (HID report size) | UHID_FRAME_NUM | ≪ 2^32 |
| ulpt/urio (usbd_xfer_max_len) | ≤ 1024×16 | IFQ maxlen (~64) | ≪ 2^32 |
| ums/wsp/uep/uaudio/ubtbcmfw | fixed constants | fixed | ≪ 2^32 |

Largest in-tree product ≈ 94 MB — two orders of magnitude below 2^32.
**No unprivileged user can reach a truncating product today.** The primitive
is real and armed for any future caller that scales either factor; the fix
is a 3-line guard.

## Reproduction (guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC)

KLD harness `usbmbuf_of.c` (in pack) calls the real in-kernel
`usb_alloc_mbufs` (symbol at ffffffff80a4f250, compiled into GENERIC) with
`block_size=0x08000000, nblocks=0x20`:

* true requirement: `(0x08000000 + 48) * 32 = 0x100000600` (4,294,968,832)
* truncated allocation: **0x600 = 1536 bytes** (console printout, panic.txt)
* all 32 mbufs enqueued; `m[0]->cur_data_ptr == base + 0x600` — exactly
  one-past-end of a 1536-byte chunk (adjacent-object corruption)
* `m[31]->cur_data_ptr == base + 0xF8000600` (~3.7 GiB past the chunk)
* one-byte write through `m[31]->cur_data_ptr`:

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8020ebade80
fault code            = supervisor write data, page not present
Stopped at usbmbuf_of_modevent+0x1cb: movb $0x41,(%rax)
```

Guest died in DDB (ssh dropped, `vm.sh status` = down). Full serial-console
transcript in panic.txt. This is proof the primitive exists; it is NOT an
unprivileged exploit — loading the harness requires root.

## Fix validation

* fix.diff: compute in `size_t`, reject `> 0xFFFFFFFFU` (return NULL).
  `git apply --check` clean against `sys/bus/u4b/usb_mbuf.c`.
* `vm.sh reset with-src` → patched guest /usr/src → `make nativekernel` →
  `make installkernel` → reboot (kernel #1, Oct 9 2026).
* Re-ran the exact harness: console shows `bytes truly required =
  0x100000600` then **`allocation FAILED (NULL)`** — no kmalloc, no queue
  population, `kldload rc=0`, guest stayed up (run.fixed.log /
  run.fixed.console.txt). Baseline panic is GONE.

## What I checked and found CLEAN (negative results with weight)

* Layout math (usb_mbuf.c:77-91): perfect fit, last data byte == end of
  allocation; no off-by-one; 48-byte header keeps data 8-aligned on x86_64.
* Alignment wrap (usb_mbuf.c:65): `block_size` in [0xFFFFFFF9..0xFFFFFFFF]
  wraps to 0 → `if (nblocks && block_size)` skips → caller sees ENOMEM via
  usb_dev.c:1996. Benign, no bypass (FreeBSD-identical code).
* M_WAITOK + NULL check (usb_mbuf.c:71-75): dead-but-harmless defensive code.
* Queue macros (usb_mbuf.h:46-84 ENQUEUE/DEQUEUE/PREPEND/QFULL/POLL):
  correct singly-linked discipline, empty-queue tail handling verified.
* Consumer copies (usb_dev.c:2064-2137, 2175-2214): all MIN(len,
  cur_data_len)-bounded under f->priv_lock.
* M_ZERO on allocation → no uninitialized-kernel-memory leak path.
* Consumer race hazard (NOT this file): `ugen_transfer_setup`
  (usb_generic.c:145-167) drops priv_lock around usbd_transfer_setup +
  usb_fifo_alloc_buffer — a second read()/write() on the same ugen fifo can
  interleave (f->xfer still NULL) and kfree queue_data mid-population.
  Same family as existing DF-5309/DF-5314/DF-5315 (usb_dev/usb_generic);
  root cause lives there, not in usb_mbuf.c.
* usb_ethernet.c (DF-5493..5495 this pass) does NOT touch this file
  (zero references to usb_mbuf/USB_IF_*/usb_alloc_mbufs).

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