# VERDICT — DF-5288

**Status: REPRODUCED** (unprivileged local user, fresh-boot guest) —
`reproduced`, impact `panic` (reliable kernel crash; the faulting pointer
is read from freed heap memory, so this is a groomable UAF, not just a NULL
deref).

## Reproduction

Guest: DragonFly 6.5-DEVELOPMENT (`with-src` snapshot), QEMU x86_64,
`-device usb-ehci` + `-device usb-mouse` (ugen0.2; control-endpoint node
`/dev/usb/0.2.0`).  Unprivileged user `maxx` (uid 1001, operator group,
0660 nodes — the standard libusb-access configuration).

`./df5288` — thread A hammers `USB_FS_START` (control-endpoint fs transfer:
GET_STATUS, nFrames=2), the main thread cycles `USB_FS_UNINIT` →
`USB_FS_INIT` → `USB_FS_OPEN`.

### Run 1 (fresh boot #1)

```
Fatal user address access from kernel mode from df5288 at ffffffff80a5438c

Fatal trap 12: page fault while in kernel mode
cpuid = 1; lapic id = 1
fault virtual address	= 0x130
fault code		= supervisor read data, page not present
instruction pointer	= 0x8:0xffffffff80a5438c
current process		= 931 (df5288)
Stopped at      usbd_transfer_pending+0x3c:     movq    0x130(%rax),%rdi
```

### Run 2 (fresh boot #2, snapshot-reverted guest, re-built binary)

```
kernel: type 9 trap, code=0
current process		= 894 (df5288)
Stopped at      usbd_transfer_pending+0x3c:     movq    0x130(%rax),%rdi
```

Both runs: guest dead in DDB, needs hard reset.
`usbd_transfer_pending` spans 0xffffffff80a54350.. (+0x3c = ...38c ✓).
The only caller reachable from this ioctl mix is
`ugen_fs_copy_in()` at sys/bus/u4b/usb_generic.c:1056 — its `xfer` local
was snapshotted from `f->fs_xfer[ep_index]` (usb_generic.c:1051) with no
lock, while `ugen_fs_uninit()` (usb_generic.c:978-981) concurrently
executed `usbd_transfer_unsetup(f->fs_xfer, f->fs_ep_max); kfree(f->fs_xfer,
M_USB); f->fs_xfer = NULL; f->fs_ep_max = 0;` — also with no lock.

`rax` in the faulting `movq 0x130(%rax),%rdi` is the stale `xfer->xroot`
load: the pointer chain runs through memory that UNINIT already freed
(the 8*N-byte `fs_xfer` array — an attacker-sprayable kmalloc — and/or the
xfer block itself).  Run 1 faulted at VA 0x130 (NULL `xroot` read out of
zeroed freed memory); run 2 took a GPF on the same instruction (garbage
pointer).  With heap grooming of the freed array (any 8..1016-byte M_USB
allocation pattern, e.g. repeated USB_FS_INIT), the stale `xfer` value is
attacker-chosen and `usbd_transfer_start()` would then write through
`xfer->xroot->bus`/`flags_int` — a write primitive chain, not pursued here
beyond the demonstrated crash.

## Why the window is trivially winnable

`ugen_fs_copy_in()` performs one `copyin()` per frame length plus one per
buffer pointer plus the endpoint struct itself — a dozen-plus separate
fault-capable copyins between the unlocked `f->fs_ep_max` check at
usb_generic.c:1048 and the `f->fs_xfer[ep_index]` re-read at
usb_generic.c:1429 — while `ugen_fs_uninit()` needs just one unlocked
`kfree()` to land inside it.

## Fix validation

See `fix_validation.md` / `fix_build.log` / `run.patched.log` — with
`fix.diff` (array detached under `priv_lock` in `ugen_fs_uninit()` +
locked revalidation in `ugen_fs_copy_in/out`, `USB_FS_START`,
`USB_FS_STOP`) applied to a rebuilt kernel, the exact same PoC survives
(the "survived N uninit cycles" path) where the unpatched kernel panicked
within seconds.

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