# DF-5656 — usb_dynamic.c unload "grace period" is a 1s tsleep, not synchronization

File audited (pass 2): `sys/bus/u4b/usb_dynamic.c` (144 LOC)

## What the bug is

`usb_quirk_unload` / `usb_temp_unload` / `usb_bus_unload`
(`sys/bus/u4b/usb_dynamic.c:101-144`) reset five globally-visible dispatch
pointers (`usb_test_quirk_p`, `usb_quirk_ioctl_p`, `usb_temp_get_desc_p`,
`usb_temp_setup_by_index_p`, `usb_temp_unsetup_p`) and then "wait for CPU to
exit the loaded functions" via `tsleep(usb_quirk_unload, 0, "WAIT", hz)` — a
**fixed 1-second sleep** that is the *only* thing standing between an
in-flight consumer and module text that `kldunload` frees immediately after
the SYSUNINIT returns. The code itself admits it: `/* XXX this is a tradeoff */`.

Consumers read the pointer and make the indirect call with **no lock**:

- `sys/bus/u4b/usb_dev.c:1710` — `usb_quirk_ioctl_p` (USB_DEV_QUIRK_GET is
  **unprivileged**: world-readable `/dev/usbctl`, no caps check on GET)
- `sys/bus/u4b/usb_device.c:2614` — `usb_test_quirk_p` (device probe path)
- `sys/bus/u4b/usb_device.c:1727` — `usb_temp_setup_by_index_p`
- `sys/bus/u4b/usb_device.c:2186` — `usb_temp_unsetup_p` (fires from
  `usbd_free_device` on every device detach, host side)
- `sys/bus/u4b/usb_request.c:475-495, 1239-1247` (via `usbd_get_hr_func`,
  gadget side) and `sys/bus/u4b/usb_handle_request.c:644`

Any consumer preempted between pointer-load and call (or blocked inside the
callee) for >1s while the module unloads calls **freed module text** (UAF).
For `usb_quirk` there is a second sink: `usb_quirk_uninit`
(`sys/bus/u4b/quirk/usb_quirk.c:1089-1095`) runs
`lockuninit(&usb_quirk_lock)` right after the grace; DragonFly's production
`lockuninit` `KKASSERT`s (panic) if any thread still holds/waits on the lock,
and the lock itself lives in module data (UAF write if the assert is compiled
out).

## Threat model / severity

The unloading party must be privileged (`kldunload`), so this is **Low**:
root can turn its own module unload into a panic or, theoretically, execution
of freed text. On GENERIC kernels (`device usb`) `usb_quirk.c` is compiled in
(`sys/conf/files:3028`), so its hooks run at shutdown only; `usb_template.ko`
is the one live-unloadable module (`sys/conf/files:3032`). On custom
all-modular USB builds the quirk race is live.

## Reproduce on the guest

```
# push quirk_get_spam.c + race_root.sh to the guest, then as root:
cd /root/df5656
cc -O2 -o quirk_get_spam quirk_get_spam.c
./race_root.sh 20 8          # quirk side (will show: usb_quirk is IN-KERNEL)
# template grace-period measurement (decisive, 12/12 unloads = exactly 1s):
i=0; while [ $i -lt 12 ]; do kldload usb_template; u0=$(date +%s); \
  kldunload usb_template; u1=$(date +%s); echo "unload=$((u1-u0))s"; i=$((i+1)); done
```

Expected (and observed): every `kldunload usb_template` blocks **1.00s** —
that second *is* the tsleep grace period, measured live. `kldload usb_quirk`
fails with "module already loaded or in kernel" on GENERIC. An unprivileged
uid=1001 process pushed 156,989 dispatch calls through `/dev/usbctl` in ~1s.

## Observed verdict

Corruption/panic **not reproduced** (window = preempted-between-load-and-call
during a root-initiated unload; not forceable unprivileged, and on this guest
the only live-unloadable module's host-side consumer fires only at bus
detach). The defect itself is certain from code and its 1s grace was measured
live. Full narrative in `VERDICT.md`.

## Fix

`fix.diff` — replaces the tsleep with a real quiescence barrier: one global
`usb_dyn_lock`; consumers hold it SHARED across pointer-read **and** indirect
call (`USB_DYN_RLOCK()`/`USB_DYN_RUNLOCK()`); unload hooks take it EXCLUSIVE
around the pointer resets. `git apply --check` clean against this tree.
