# DF-5301 — uhub_child_location_string/pnpinfo_string TOCTOU: NULL `usb_hub*` deref (and UAF) during hub detach

## What

`uhub_child_location_string()` and `uhub_child_pnpinfo_string()`
(sys/bus/u4b/usb_hub.c:1660-1757) validate the hub device with
`device_is_attached(parent)` and then dereference
`sc->sc_udev->hub` — a pointer that `uhub_detach()`
(sys/bus/u4b/usb_hub.c:1592) sets to NULL (and kfrees at :1590)
while the newbus device is **still** `DS_ATTACHED`: in DragonFly's
`device_detach()` (sys/kern/subr_bus.c:2129-2153) the state only
becomes `DS_NOTPRESENT` *after* `DEVICE_DETACH()` returns (and after
`device_printf(dev, "detached\n")` and devclass bookkeeping).

A reader that passes `device_is_attached()` and then reads
`hub == NULL` crashes in `uhub_find_iface_index()`:

```
sys/bus/u4b/usb_hub.c:1637
	nports = hub->nports;      /* hub == NULL -> kernel NULL-deref panic */
```

A reader that lands between `kfree(hub)` (:1590) and the NULL store
(:1592) gets a use-after-free read; readers that resolve
`bus->devices[up->device_index]` without `usb_ref_lock`
(`usb_bus_port_get_device()`, sys/bus/u4b/usb_hub.c:2190-2201) can
additionally walk a freed `usb_device` (`usbd_get_iface`, serial string
in `%pnpinfo`) — heap data from freed objects can surface in the
user-readable string.

## Reachability (unprivileged)

The methods are dispatched from `bus_child_location_str()` /
`bus_child_pnpinfo_str()` (sys/kern/subr_bus.c:3434-3460), which are
called from world-readable sysctls:

* `dev.<devclass>.<unit>.%location` / `%pnpinfo` — per-device text
  sysctls (sys/kern/subr_bus.c:209-218), e.g. `dev.uhid.0.%location`
  where `uhid0`'s parent is the detaching `uhub1`.
* `hw.bus.devices` — the global unlocked device-tree walk
  (sys/kern/subr_bus.c:3873-3905) which formats location+pnpinfo for
  every device in the system.

No privilege is needed to read these.  The racing kernel side is any
hub detach: physical unplug on real hardware, or (in this repro)
QEMU monitor `device_del` of an emulated hub.

## Repro environment

* Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), 6 vCPU KVM.
* QEMU started with `-device piix4-usb-uhci,id=uhci0`
  (`QEMU_EXTRA="-device piix4-usb-uhci,id=uhci0" dfbsd-qemu/vm.sh up`).
* Devices via monitor:
  * `device_add usb-hub,id=h1,bus=uhci0.0,port=1`   -> guest `uhub1`
  * `device_add usb-tablet,id=ta,bus=uhci0.0,port=1.1` -> `uhid0` (child of uhub1)
  * `device_add usb-tablet,id=tb,bus=uhci0.0,port=1.2` -> `uhid1` (child of uhub1)
* Verify dispatch: `sysctl dev.uhid.0.%location` prints
  `bus=0 hubaddr=2 port=1 devaddr=3 interface=0 ugen=ugen0.3`
  — served by uhub1's `uhub_child_location_string`.

## Files

* `hammer.c`   — unprivileged reader storm (threads hammering the
                  vulnerable sysctls + the global walk), built/run as
                  regular user `maxx`.
* `replug.py`  — host-side replug driver (monitor device_del/add cycle).
* `build.sh`, `run.sh` — exact commands.
* `run.log`, `panic.txt`, `boot-snippet.txt` — runtime evidence.
* `fix.diff`   — post-verification fix (NULL-guard `hub` in both
                  methods), never applied to the repo `sys/` tree.

## Expected result

Kernel panic (`Fatal kernel mode data access fault` / NULL deref in
`uhub_find_iface_index`) while the hammer runs and a hub replug cycle
lands in the window; guest drops to DDB, ssh dies.  It is a race —
see VERDICT.md for hit-rate math and observed cycles.
