# VERDICT — DF-5287

**Status: REPRODUCED** (unprivileged local user, fresh-boot guest, no kernel
modifications) — `reproduced`, impact `dos` (kernel memory leak +
permanent endpoint lockout; same race also opens an unlocked
`usb_fifo_free_buffer()` UAF window, documented below).

## How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT (`with-src` snapshot), QEMU x86_64,
`-device usb-ehci` + `-device usb-mouse` (ugen0.2, interrupt-IN endpoint
node `/dev/usb/0.2.1`).  Unprivileged user `maxx` (uid 1001, member of
`operator`, nodes chmod 0660 — the standard BSD "libusb user" configuration;
default DragonFly ships these nodes 0600 root:operator).

1. Baseline: `vmstat -m` → `USB 16 allocs / 19.8K`.
2. `./df5287 3000` — two threads race `read()` on one shared
   O_RDONLY|O_NONBLOCK fd, close+reopen per iteration.
3. After 2938 iterations the PoC printed its marker:
   `EIO after 2938 iterations (endpoint refcount_alloc exhausted by
   orphaned usb_xfers -- double-setup race won >= 61 times)`.
4. `vmstat -m` → `USB 331 allocs / 1.10M` (2.95K requests): ~1 MB of
   never-freeable kernel memory accumulated from the orphaned xfer blocks.
5. Re-running the PoC immediately EIOs at iteration 0 — the endpoint stays
   dead (refcount_alloc pinned at USB_EP_REF_MAX) until the device is
   re-plugged; `usbconfig dump_info` still shows a healthy device, so this
   is a silent lockout of the endpoint for every consumer, kernel drivers
   included.
6. Write-side variant (`df5287c`, usb-storage bulk-OUT endpoint 0.2.2,
   umass not attached): same leak signature — M_USB grew 34.3K → 1.24M
   within ~1000 iterations before the run was stopped (log `run.3.log`).

## Why the leak is permanent

Each race-won iteration leaves one `usbd_transfer_setup()` allocation
unreferenced:

* `usbd_transfer_setup()` incremented `xfer->endpoint->refcount_alloc`
  (sys/bus/u4b/usb_transfer.c:1121) and `info->setup_refcount` (usb_transfer.c:1132);
* the *second* setup's entry loop wrote `ppxfer[n] = NULL` over the first
  setup's pointers (usb_transfer.c:935 — `ppxfer[n] = NULL;` in the initial
  sanity loop), so `usbd_transfer_unsetup()` never sees them again;
* therefore neither `refcount_alloc--` (usb_transfer.c:1421) nor
  `info->setup_refcount--` (usb_transfer.c:1431) ever runs for the orphan,
  and the memory block is never freed.

## The UAF window (same root cause, harder to time)

`ugen_transfer_setup()` calls `usb_fifo_alloc_buffer()` ->
`usb_fifo_free_buffer()` (sys/bus/u4b/usb_dev.c:2009-2020) *after*
re-acquiring... actually *before* re-acquiring `priv_lock`
(usb_generic.c:145-166 in the unfixed tree: the buffer swap sits between
`lockmgr(f->priv_lock, LK_RELEASE)` and the final
`lockmgr(f->priv_lock, LK_EXCLUSIVE)`).  `usb_fifo_free_buffer()` does
`kfree(f->queue_data)` + `memset` of both queues with no lock at all, while
the winning setup's live callbacks (`ugen_ctrl_read_callback` ->
`usb_fifo_put_data()`, usb_dev.c:2064; `ugen_ctrl_write_callback` ->
`usb_fifo_get_data()`, usb_dev.c:2176) iterate those queues under
`priv_lock`.  A racing second setup therefore frees the mbuf storage under
a running callback: writes into freed M_USBDEV heap + corrupted queue
linkage feeding `read()`/`write()`.  This window was not converted into a
demonstrated panic within the time budget (it needs a callback in flight
inside a ~µs-wide free window; the mouse endpoint provides no traffic in a
headless QEMU and the storage write-path was used for the leak runs), but
it follows directly from the same unlocked code path.  The fix removes it.

## Fix validation

See `fix_validation.md` / `fix_build.log` / `run.patched.log` — with
`fix.diff` (busy-flag in `ugen_transfer_setup()` + fifo buffer swap under
`priv_lock`) applied to a rebuilt kernel, the exact same PoC ran to
completion without the EIO marker and without M_USB growth.

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