# VERDICT — DF-5299 (and the usb_device.c pass-2 soak)

## Finding DF-5299 — `usbd_get_phys()` type-confused `pci_devinfo *` deref

**Status: inconclusive (runtime), certain (source). Severity Low, confidence
speculative (impact), certain (defect exists). Not reproduced — the trigger is
architecturally absent on the x86-64 test target.**

### Root cause (source-verified)

`sys/bus/u4b/usb_device.c:2249-2258`:

```c
struct pci_devinfo *info;
...
info = device_get_ivars(device_get_parent(udev->bus->bdev));
ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
        info->cfg.domain, info->cfg.bus,
        info->cfg.slot, info->cfg.func);
```

The USB bus device's (`usbusN`) parent is the host-controller `device_t`.
The code *assumes* that controller is a PCI child and dereferences its
ivars as `struct pci_devinfo *` with **no bus-type check and no NULL
check**:

* if the controller's parent provides no ivars (or `device_get_parent()`
  returns NULL — controller on the root nexus), `info` is NULL/garbage and
  `info->cfg` is a **NULL/wild deref → kernel panic** at attach of the first
  UVC device on that bus;
* if the controller hangs off any non-PCI bus whose ivars are a different
  struct, four attacker-irrelevant but kernel-controlled words of adjacent
  heap are formatted into `phys`, which is copied to userspace by
  `sys/bus/u4b/uvc/uvc_v4l2.c:132` → `VIDIOC_QUERYCAP.bus_info` — a
  **limited kernel info leak**.

Sole caller: `uvc_v4l2.c:132` (UVC attach / QUERYCAP). On DragonFly x86-64
every compiled HCI (uhci/ehci/xhci/ohci) is a PCI device, so the branch is
unreachable there; it is live on embedded targets (AVR32/AT91/MUSB/Octeon
OTG controllers in `sys/bus/u4b/controller/`) and for any future non-PCI
HCI (e.g. ACPI-described MMIO controllers).

### Why not runtime-verified here

The QEMU guest exposes USB only via `qemu-xhci`/`usb-ehci`, both PCI. A UVC
device is additionally required (QEMU does not emulate one; `usbd_get_phys`
is UVC-only). Neither precondition can be constructed on this rig, so no
runtime verdict is possible — `inconclusive`, per the honest-status rules.

### Recommended fix

Guard the deref: only format the PCI tuple when the grandparent is a PCI
bus (or use `pci_get_<domain/bus/slot/func>` style accessors after a type
check), else fall back to a bus-unit-only string:

```diff
--- a/sys/bus/u4b/usb_device.c
+++ b/sys/bus/u4b/usb_device.c
@@ -2246,7 +2246,11 @@ usbd_get_phys(struct usb_device *udev, char *phys, uint8_t len)
 	if (!phys)
 		return;
 
-	info = device_get_ivars(device_get_parent(udev->bus->bdev));
-	ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
-			info->cfg.domain, info->cfg.bus,
-			info->cfg.slot, info->cfg.func);
+	info = device_get_ivars(device_get_parent(udev->bus->bdev));
+	if (info != NULL && device_get_devclass(device_get_parent(
+	    udev->bus->bdev)) == devclass_find("pci"))
+		ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
+				info->cfg.domain, info->cfg.bus,
+				info->cfg.slot, info->cfg.func);
+	else
+		ksnprintf(phys, len, "usb-%u-", device_get_unit(udev->bus->bdev));
```

## The usb_device.c pass-2 soak (file-level negative-result evidence)

Rig: QEMU/KVM DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, build of
2026-07-02) with `qemu-xhci` (usbus0, root hub only — every downstream
device failed enumeration, exercising the `usb_alloc_device` error path)
and `usb-ehci` (usbus1) with `usb-storage` (ugen1.2) and `usb-tablet`
(ugen1.3).

* `usbsoak.c` (this pack): 3 threads × 2×90 s —
  USB_SET_ALTINTERFACE/SET_CONFIG/GET_FULL_DESC/DO_REQUEST/
  IFACE_DRIVER_DETACH with values including 31/255/random on
  `/dev/usb/1.2.0` and `/dev/usb/1.3.0`, plus a reopener loop on
  `/dev/usb/1.3.1`.
* Concurrent QEMU-monitor replug storm: 18 slow (~2.4 s gap) + 30 rapid
  `device_del tab0`/`device_add usb-tablet,...` cycles (device-index reuse
  window: `usb_free_device` → async cdev destroy → `usb_alloc_device` →
  `usb_make_dev` same names).

Observed totals: **139,053 ioctls, 1,559 successful full
unconfigure/configure cycles, 90 successful alt changes, 48 replugs; zero
panics, zero devfs duplicate-name warnings ( DragonFly devfs refuses
duplicates gracefully — `devfs_core.c:2111-2117` — the collision window is
functional-only), guest healthy after both runs.**

### Classes hunted and killed in this pass (path:line)

* Descriptor fetch/cache: `usbd_req_get_desc` length negotiation is
  monotonic-shrink, all writes bounded by caller max_len
  (usb_request.c:1043-1070); `usbd_req_get_config_desc_full` forces
  `wTotalLength = len` after the kmalloc (usb_request.c:1358-1376);
  langid parse bounded `scratch_ptr[0] ≤ 4` (usb_device.c:1785-1819);
  string parse `n ≤ temp[0]/2-1 ≤ 126 < 255` scratch
  (usb_request.c:1127-1182, usb_device.h:178).
* `usb_config_parse` tables: endpoint slot index checked
  `temp < USB_MAX_EP_UNITS(32)` *before* `udev->endpoints + temp`
  (usb_device.c:802-807); `ep_max ≤ 32` == alloc size (usb_device.c:845);
  iface count bounded `USB_IFACE_MAX(32)` (usb_parse.c:156); ALLOC/INIT
  re-parse deterministic over same cdesc. The zero-before-verify defect
  (incl. its "IN_USE continues zeroing" variant) is DF-1004 — not
  re-reported.
* State machine teardown: `endpoints_max`/`ep_curr` mutated only under
  USB_BUS_LOCK (usb_device.c:855-871) and re-read per invocation by
  `usb_do_clear_stall_callback` (usb_request.c:234-332) — no cross-lock
  stale-pointer UAF; `usb_free_device` order (state=DETACHED under
  usb_ref_lock at 2150/2832 before any ref taker, `usb_wait_pending_refs`
  sets REF_MAX under the lock at 462-473) closes the ref race;
  `usbd_transfer_unsetup` of ctrl_xfer precedes final free with
  `usb_proc_mwait(cs_msg)` (2192-2195).
* Refcount/devfs lifecycle: `usb_ref_device` re-resolves
  `bus->devices[dev_index]` under `usb_ref_lock` with DETACHED gate
  (usb_dev.c:200-225); pd stores indices, not udev pointers
  (usb_device.c:1978-1982) — replug cannot produce a stale udev deref;
  fast-replug cdev name collision resolves as devfs "already exists,
  ignored" (functional only).
* Peer/hub relations: depth capped ≤ 0x10 before alloc (1575);
  device_index scan bounded by `devices_max` (1564-1573);
  `hs_hub` walk bounded by depth (1642-1654); `usbd_get_phys` no[] walk
  bounded by depth ≤ 16 < 128 (2260-2264).
* Quirk matching: `usb_test_quirk` bounded scan of `autoQuirk[]`
  (2596-2617); `usbd_add_dynamic_quirk` bounded (2988-3001).
* Do-request sizes: all `usbd_do_request*` from this file pass
  stack/scratch buffers sized ≥ wLength; timeout clamped 50..30000 ms
  (usb_request.c:421-428).
* ugen open/ioctl interplay: every path takes the udev ref + enum lock
  (usb_dev.c:231), serializing against `usb_unconfigure`/`usb_free_device`;
  `ugen_set_config` is deliberately async (usb_generic.c:614-624) to avoid
  the fifo-drain self-deadlock; SET_ALT skips EP0 fifos
  (usb_device.c:2781-2785).

## Companion finding DF-5300 (Info, no runtime aspect)

`usbd_get_iface()` computes `udev->ifaces + iface_index` **before** the
`iface_index >= udev->ifaces_max` bounds check (usb_device.c:2237-2244).
Never dereferenced when OOB, but the OOB (and NULL-base) pointer
arithmetic executes on every call with an unvalidated index — C UB and a
loaded footgun for future callers. Hardening: move the addition after the
check. No PoC (not a runtime defect).

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