# DF-2346 — SIGIO recipient stored as raw unreferenced struct proc * (usb_dev.c)

## Verdict: NOT REPRODUCED (HW-gated); REAL BUG IN SOURCE (defense-in-depth fix warranted)

## Hardware gate (why the PoC cannot run on this guest)

The vulnerable field is `f->async_p` on a **USB FIFO** (`struct usb_fifo`),
which is created per USB device endpoint and exposed as a `/dev/ugen<N>.<ep>`
character device. The audit QEMU/KVM guest has **no USB device attached**, so
there are no FIFOs and no ugen nodes:

```
$ usbconfig list                  # No device match or lack of permissions.
$ ls /dev/ugen*                   # No such file or directory
$ ls /dev/usb*                    # only /dev/usbctl
$ ls -la /dev/usbctl              # crw-r--r--  root operator  (no group write)
$ id maxx                         # uid=1001 gid=1001 groups=1001  (NOT in operator)
```

Direct probe run as `maxx` (see `maxx_open_probe.txt`):
```
open("/dev/usbctl", O_RDWR)   -> -1     (EPERM: write needs root)
open("/dev/usbctl", O_RDONLY) -> 3      (read OK, but read-only fd is useless here)
```
Even the read-only fd does **not** reach the bug: `f->async_p =
USB_TD_GET_PROC(td)` lives in the **per-FIFO** `usb_ioctl()` case `FIOASYNC`
(`usb_dev.c:1038-1044`), dispatched on a ugen endpoint fd. `/dev/usbctl`'s ioctl
entry is `usb_static_ioctl` (bus/device enumeration control ioctls), not the
FIFO ioctl, and `FIONBIO`/`FIOASYNC` on usbctl are "handled by upper FS layer"
(`usb_dev.c:1035`). With no USB device → no ugen endpoint → no USB FIFO → the
`f->async_p = USB_TD_GET_PROC(td)` assignment is unreachable by any user.

The bug's trigger (fork + parent exit + child USB completion) additionally
requires an active async transfer on an open ugen FIFO, which presupposes the
USB device the PoC relies on.

## Source trace — the bug is REAL (sys/bus/u4b/usb_dev.c)

`struct usb_fifo` carries (`usb_dev.h:110`):
```c
struct proc *async_p;     /* process that wants SIGIO */
```
assigned in the per-FIFO `usb_ioctl` (`usb_dev.c:1038-1048`):
```c
case FIOASYNC:
    if (*(int *)addr) {
        if (f->async_p != NULL) { error = EBUSY; break; }
        f->async_p = USB_TD_GET_PROC(td);   /* usb_dev.c:1044 */
    } else
        f->async_p = NULL;
    break;
```
`USB_TD_GET_PROC(td)` = `td->td_proc` (`usb_dragonfly.h:58`) — a **raw** `struct
proc *` taken with **no reference** (no `PHOLD()` / refcount increment) and **no
registration** on `proc->p_sigiolst`.

It is then dereferenced locklessly in `usb_fifo_wakeup` (`usb_dev.c:1789-1791`):
```c
if (f->async_p != NULL && lwkt_trytoken(&f->async_p->p_token)) {
    ksignal(f->async_p, SIGIO);
    lwkt_reltoken(&f->async_p->p_token);
}
```
and the same pattern in `usb_fifo_close` (`usb_dev.c:798-801`).

Every other async-signal character device in the tree (pipe, socket, tty, bpf,
tap, tun, kqueue, evdev, drm) uses the refcounted `struct sigio` / `fsetown` /
`pgsigio` mechanism. USB bypasses it. After `fork()`, the child inherits the
open ugen FIFO with `async_p == parent's proc`; the parent can `exit()` and be
reaped, freeing the `proc` struct. The next USB transfer completion calls
`usb_fifo_wakeup` → `lwkt_trytoken(&f->async_p->p_token)` on a freed proc →
UAF. Floor: reliable kernel panic / local DoS. Ceiling: `proc` slab reuse →
heap grooming for code execution.

Attacker: operator-group/desktop user opens a ugen endpoint, enables FIOASYNC,
forks, parent exits; child triggers a completion. Requires an attached USB
device providing an endpoint FIFO.

## Exploit chain status

Not pursuable — primitive (lockless UAF on `struct proc`) requires an open ugen
FIFO, which requires an attached USB device (absent) — valid Phase-6 hard
blocker: dead path at runtime on this guest. On a desktop with a USB device
plugged in this is a real write-capable UAF.

## PoC changes

Authored a small reachability probe (`maxx_open_probe.txt`) confirming maxx
cannot get a writable FIFO fd on this guest. No exploit chain written: no USB
device → no FIFO → no `async_p` assignment.

## Recommended fix

Convert `f->async_p` to the refcounted `struct sigio *` mechanism (`fsetown`/
`pgsigio`/`funsetown`), matching every other async-signal char device. See
`fix.diff` for a minimal defense-in-depth fix that takes a proc reference
(`PHOLD`) on assignment and releases it (`PRELE`) on clear/close, closing the
UAF (a full sigio refactor is the proper long-term fix).
