# VERDICT — DF-5314

**Status: REPRODUCED** (unprivileged local user, fresh boots, no kernel
modifications) — `reproduced`, impact `panic` (kernel assertion failure
from a double `usbd_transfer_unsetup()` of the same xfer; on kernels
without `USB_ASSERT` compiled in the same race instead underflows
`endpoint->refcount_alloc` to 0xFF and permanently locks the endpoint
out for every consumer). The finding covers the **unserialized
`USB_FS_CLOSE`** side of the `f->fs_xfer[]` state machine in
`ugen_ioctl()` — the sibling of DF-5288 whose freeing side there is
`USB_FS_UNINIT`.

## What the finding is

`ugen_ioctl()` dispatches the whole `USB_FS_*` family with **no
serialization** (the cdev ioctl path takes no fifo lock,
sys/bus/u4b/usb_dev.c:1115, and no enum lock for `f_ioctl`,
usb_dev.c:1094).  Specifically `USB_FS_CLOSE`
(sys/bus/u4b/usb_generic.c:1558-1568) does:

```c
usbd_transfer_unsetup(f->fs_xfer + u.pclose->ep_index, 1);
```

and `usbd_transfer_unsetup()` reads its array entry **before** taking
the xfer lock (sys/bus/u4b/usb_transfer.c:1379 `xfer = pxfer[n_setup];`
… :1386 `USB_XFER_LOCK(xfer);` … :1405 `pxfer[n_setup] = NULL;`).  Two
consequences:

1. **CLOSE × CLOSE double-unsetup (reproduced, panics).**  Two
   concurrent closes that both read the non-NULL pointer before either
   NULLs it both run a full unsetup on the same xfer:
   `xfer->endpoint->refcount_alloc--` twice (usb_transfer.c:1421,
   1 → 0 → 0xFF) and `info->setup_refcount--` twice (:1431).  On this
   kernel `USB_ASSERT(info->setup_refcount != 0, …)` (usb_transfer.c:1428)
   is live, so the second decrement panics:

   ```
   panic: Invalid setup reference count
   usbd_transfer_unsetup() at usbd_transfer_unsetup+0x16c
   usbd_transfer_unsetup() at usbd_transfer_unsetup+0x16c
   ugen_ioctl() at ugen_ioctl+0x109
   ```

   (`panic.txt`; hit 3/3 times on fresh boots — twice during
   exploration, once in the documented run where the spin-gated PoC
   `df5314c` panics in **round 1**, `run.2.log`.)  On a kernel without
   that assert the result is the underflow: `refcount_alloc` pinned at
   0xFF ≥ `USB_EP_REF_MAX` (0x3F) → the endpoint permanently rejects
   every `usbd_transfer_setup()` (usb_transfer.c:1118-1119) for ugen
   ioctls, read()/write() and kernel drivers alike, until replug.

2. **CLOSE × START/COMPLETE stale-xfer use (code-proven, shallow on
   this kernel).**  `ugen_fs_copy_in()`/`ugen_fs_copy_out()` snapshot
   `xfer = f->fs_xfer[ep_index]` with no lock (usb_generic.c:1051,
   :1229 — the same consumer sites DF-5288 identified for UNINIT) and
   then write transfer parameters into the xfer across several sleeping
   `copyin()`s (:1069-1199: `xfer->nframes`, `xfer->timeout`, flag
   writes, `usbd_xfer_set_frame_offset()` pointer writes, and for
   control transfers `copyin(uaddr, req, 8)` writing through
   `req = xfer->frbuffers[0].buffer` read from the freed block).  A
   concurrent `USB_FS_CLOSE` logically frees the xfer mid-loop.

   **Why variant A does not crash on the shipped kernel** (honest
   negative, `run.log`: 3072+ cycles survived twice): DragonFly kmalloc
   zones are *per-malloc-type, single-size* (sys/kern/kern_kmalloc.c:42-52
   — "zones don't stomp over each other"), so the freed M_USB block can
   only be reclaimed by another same-shaped xfer block (benign
   aliasing), never by attacker-controlled foreign data; and
   `usbd_transfer_unsetup_sub()` releases the memory only through the
   256-slot delayed-free ring (`usbd_delayed_free`,
   usb_transfer.c:1333-1357), which keeps the content intact long past
   the race window.  The stale writes therefore land in
   logically-freed but intact (or same-typed reallocated) memory.  The
   primitive is real — an unprivileged user writing attacker-chosen
   `nframes`/`frlengths`/request bytes into freed kernel memory — but
   on this kernel configuration it does not produce a user-visible
   fault, so it is documented rather than claimed.

   Variant B (`df5314b.c`, barrier-synchronised double-close) exercised
   the same window as variant C with microsecond-scale entry skew and
   did not land in 5000 rounds (`[!] round 2` note: the first harness
   version had a barrier-count bug; fixed version ran clean); the
   spin-gate variant C compresses the entry skew to ~100 ns and lands
   deterministically (round 1).

## Reproduction (variant C, `df5314c.c`)

Guest: DragonFly 6.5-DEVELOPMENT (`with-src` snapshot, kernel #0),
QEMU x86_64, `-device usb-ehci` + `-device usb-mouse`; unprivileged
`maxx` (uid 1001, not in operator) on `/dev/usb/0.2.0` chmod 0666.

1. `USB_FS_INIT` (ep_index_max=4) + `USB_FS_OPEN` (ep 0, control).
2. 4 closer threads spin on a gate; the main thread releases it so all
   fire `USB_FS_CLOSE` within ~100 ns of each other, then re-arms with
   `USB_FS_OPEN`.
3. Round 1: two closers both read `pxfer[0]` before either NULLs it →
   double unsetup → `panic: Invalid setup reference count`, guest dead
   in DDB (`panic.txt`).  3/3 fresh-boot baseline runs panicked.

## Fix validation (fix.diff, this pack)

`fix.diff` (19 hunks, self-contained) adds a slot-claim protocol:
`USB_FS_CLOSE` now clears the slot under `priv_lock` and unservices the
*claimed* pointer, so exactly one closer ever runs an unsetup; it also
carries the DF-5288-style `priv_lock`-held `ugen_fs_copy_in/out()`
(poison-aware), the `USB_FS_OPEN` setup claim (DF-5315), the
`USB_FS_STOP` `u.pstart`→`u.pstop` typo fix, a locked snapshot in
`USB_FS_CLEAR_STALL_SYNC`, and `ugen_fs_uninit()` locking.  Built and
booted (`fix_build.log`, kernel #1 15:25:39 UTC):

| check | baseline | patched |
|---|---|---|
| `df5314c` (3000 spin-gated double-close rounds) | panic in round 1 (3/3 boots) | **SURVIVED 3000/3000**, guest up |
| `df5315` (double-open) | endpoint dead, 1 MB leaked | serialised, no exhaustion (`run.patched.log`) |
| `df5288` (regression) | panics ≤ 60 s | survived 100 s (rc=124) |
| FS smoke (libusb-style GET_STATUS ×5) | — | **PASS 5/5** (no functional regression) |
| `vmstat -m` USB after full suite | 331 allocs / 1.02 MB after 20 s of racing | 270 / 844 KB after a much heavier suite (no orphans) |

## Exploitability notes

* Panic from an unprivileged user: trivial (variant C).
* The underflow face (no-assert kernels) is a permanent endpoint lockout
  (same impact class as DF-5287's exhaustion half).
* The stale-write face (variant A) is a genuine use-after-free write of
  semi-controlled data into freed kernel memory; on the shipped
  per-type-zone kernel it is not convertible to a crash with the
  primitives available to an unprivileged user (no cross-type heap
  spray can reach an M_USB zone).  Should the allocator ever share
  zones by size class, this becomes a groomable write primitive.

## Kernel references

- sys/bus/u4b/usb_generic.c:1558-1568 (USB_FS_CLOSE, no lock)
- sys/bus/u4b/usb_generic.c:1051-1060, :1229-1238 (unlocked consumer snapshots)
- sys/bus/u4b/usb_generic.c:1590 (unlocked `->endpoint` deref)
- sys/bus/u4b/usb_transfer.c:1379-1405 (read-before-lock in unsetup)
- sys/bus/u4b/usb_transfer.c:1421, :1428-1431 (refcount decrement + assert)
- sys/bus/u4b/usb_transfer.c:1333-1357 (delayed-free ring)
- sys/kern/kern_kmalloc.c:42-52 (per-type single-size zones)

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