# DF-5584 VERDICT — REPRODUCED (logic-level; no memory-safety impact in current consumers)

## The defect

`sys/bus/u4b/usb_parse.c:142-151` — `usb_idesc_foreach()` counts alternate
settings of an interface in `ps->iface_index_alt`, a **uint8_t**
(`sys/bus/u4b/usbdi_util.h:37`). When a configuration descriptor contains
**more than 256 interface descriptors sharing one `bInterfaceNumber`**
(fully attacker-chosen bytes from a malicious USB device), the
`ps->iface_index_alt ++` at `usb_parse.c:150` wraps 255 → 0 on the 257th
descriptor, so the 257th+ alternate settings are walked as if they were
alt 0, 1, 2, … again.

DragonFly's tree (imported at FreeBSD r250204, 2013) predates the upstream
fix: FreeBSD commit `6da5df470099fc55f20b03ef364af730e7be4ffe`
("Improve handling of alternate settings in the USB stack.", hselasky,
2020-12-15, PR 251856, submitted by Horse Ma / Dell) adds exactly the
`if (ps->iface_index_alt == 255) continue;` guard. Diff of DF's file vs
FreeBSD head shows this is the **only** functional divergence.

## Reproduction (guest-live, real kernel symbols)

Harness: `altwrap.c` KLD calling the **real** in-kernel
`usb_idesc_foreach()` / `usbd_get_no_alts()` (symbols confirmed in
/boot/kernel/kernel: `ffffffff80a508b0 T usb_idesc_foreach`,
`ffffffff80a50a00 T usbd_get_no_alts`) on crafted blobs:

- EVIL blob: valid 9-byte UDESC_CONFIG header (wTotalLength=2709) +
  300 interface descriptors, all `bInterfaceNumber=0`, unique
  `bAlternateSetting`/subclass tag per descriptor.
- CTRL blob: same with exactly 256.

Observed (run.log, run.2.log — deterministic across 2 loads):

```
EVIL idesc#256: iface_index_alt=255
EVIL idesc#257: iface_index_alt=0     <-- 8-bit WRAP
EVIL walk ended: max_alt=255, WRAP_AT=257
usbd_get_no_alts()=255 while true alt count=300
CTRL walk ended: max_alt=255, WRAP_AT=0   (boundary control clean)
RESULT: WRAP_REPRODUCED
```

Note attempt 1 panicked the guest — **my harness's own bug** (passing
`NULL` as `plen` to `build_blob()` → `*plen = total` NULL write inside
the KLD, before any parser call). Fixed; guest reset; attempt 2 and 3
clean. The parser was never implicated.

## Downstream effect (code-traced; replica demonstrated)

`usb_config_parse()` (sys/bus/u4b/usb_device.c:753-826) consumes the walk:

- usb_device.c:775 `if (ips.iface_index_alt == 0) ep_curr = ep_max;` —
  the wrapped 257th descriptor is treated as a new base setting: the
  interface's endpoint slot base is re-derived mid-walk, so the wrapped
  alt's endpoints occupy fresh slots above the interface's real base.
- usb_device.c:760-770 — a full USB_CFG_INIT selecting alt 0
  (`usbd_set_config_index()` → usb_device.c:648) has `do_init == 1` for
  BOTH the true alt-0 descriptor AND the wrapped 257th; last match wins
  `iface->idesc = id` (usb_device.c:782), so the interface's canonical
  descriptor — the one every driver probes against via
  `usbd_get_interface_descriptor()` — becomes the attacker's 257th
  choice (class/subclass/protocol/bNumEndpoints confusion, mismatched
  against the alt the stack actually negotiates).
- The replica of that matching loop over the real walk confirms:
  `do_init matched 2 descriptors; iface->idesc would land on the wrapped
  descriptor (ALIASED)`.
- `usbd_get_no_alts()` (usb_parse.c:267) returns 255 for a true count of
  300, so the SET_INTERFACE validator usb_handle_request.c:193 and the
  ugen ALT_SELECT path (usb_generic.c:644) reason on a count that no
  longer matches the walked reality.

## Why NOT memory corruption (impact ceiling)

All consumer arrays remain bounded regardless of the wrap:

- interfaces: `ps->iface_index >= USB_IFACE_MAX` check at usb_parse.c:156;
  allocation `ifaces_max` from the same walk (usb_device.c:831-841).
- endpoints: `temp >= USB_MAX_EP_UNITS` break at usb_device.c:802;
  allocation `ep_max` from the same deterministic walk
  (usb_device.c:844-856). ALLOC and INIT perform byte-identical walks of
  the same `udev->cdesc` under `enum_lock` (usb_device.c:643-648), so the
  wrap affects both passes symmetrically — indices never exceed the
  allocated counts.
- Every walked descriptor is in-bounds by `usb_desc_foreach()`'s own
  checks (usb_parse.c:89-99), and `cdesc->wTotalLength` is forced to the
  allocation size at fetch (usb_request.c:1366-1376).

Conclusion: attacker-triggerable **correctness/invariant break with
driver-confusion potential** (and a divergence from a validated upstream
security fix), but no OOB write/read, UAF, or reachable panic was
demonstrable from it in this tree. Escalation to uid0: not applicable
(no corruption primitive).

## Fix

`fix.diff` in this directory: byte-for-byte port of upstream FreeBSD
6da5df47. With the fix, the 257th+ same-numbered descriptor is skipped
(`continue`) and the walk terminates at the 256th — the CTRL-blob
behavior. Kernel-rebuild validation was not performed (per audit policy,
rebuild validation is reserved for memory-corruption findings; this is a
Low-severity logic/hardening finding and the hunk is already main-line
validated upstream). fix_status: not_testable.

## Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC), kernel built from this tree. Guest reset to
`with-src` snapshot after the run.

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