# DF-5656 — VERDICT

**Status: not_reproduced (mechanism proven live; corruption not manifested).**
**Impact: none observed. Finding stands: Low (root-gated race), CWE-367/CWE-416.**

## 1. What was audited

`sys/bus/u4b/usb_dynamic.c` — the whole file: five core-owned stubs, the six
dynamic dispatch globals, and the three SYSUNINIT unload hooks
(`usb_quirk_unload` :101-114, `usb_temp_unload` :116-130, `usb_bus_unload`
:132-144). The task prompt's guessed symbols (`usb_load_one_unload`,
`usb_peer_restore`) do not exist anywhere in `sys/` (verified by grep).

## 2. The defect (code-evident)

All three hooks follow the pattern:

```c
usb_test_quirk_p = &usb_test_quirk_w;      /* reset dispatch pointers */
usb_quirk_ioctl_p = &usb_quirk_ioctl_w;
/* wait for CPU to exit the loaded functions, if any */
/* XXX this is a tradeoff */
tsleep(usb_quirk_unload, 0, "WAIT", hz);   /* usb_dynamic.c:113,129,143 */
```

After the SYSUNINIT returns, `kldunload` (linker_file_unload) frees the
module's text and data. The 1-second `tsleep` is not a synchronization
primitive: a consumer that loaded the old pointer into a register before the
reset and is then delayed >1s — preempted between pointer read and indirect
call, or blocked >1s inside the callee — resumes and calls/jumps into freed
module text. No wakeup(3) exists for the `usb_quirk_unload` ident anywhere in
`sys/` (grep), so the sleep always burns the full second; nothing re-checks
anything after it.

Unlocked consumer sites (pointer read + indirect call, no barrier, no refcount):

| site | pointer | reachability |
|---|---|---|
| usb_dev.c:1710 | `usb_quirk_ioctl_p` | **unprivileged**: USB_DEV_QUIRK_GET/USB_QUIRK_NAME_GET need no privilege (world-readable `/dev/usbctl`, no caps check in `usb_static_ioctl`; only ADD/REMOVE are gated) |
| usb_device.c:2614 (`usb_test_quirk`) | `usb_test_quirk_p` | probe path, explore thread (device hotplug) |
| usb_device.c:2186 (`usbd_free_device`) | `usb_temp_unsetup_p` | every device detach (bus teardown) |
| usb_device.c:1727 | `usb_temp_setup_by_index_p` | gadget-side; dead on host-only HW |
| usb_request.c:475-495, 1239-1247 | `usb_temp_get_desc_p` (via `usbd_get_hr_func`) | gadget-side; dead on host-only HW |

Second sink for the quirk module: `usb_quirk_uninit` (quirk/usb_quirk.c:1089)
calls `usb_quirk_unload(arg)` then `lockuninit(&usb_quirk_lock)`.
`lockuninit` (kern/kern_lock.c) carries a production `KKASSERT` that the lock
has no holders/waiters — a thread still inside `usb_test_quirk_by_info`
(usb_quirk.c:735, holds `usb_quirk_lock` across the table scan) or blocked on
it when the grace expires trips the assert → guaranteed panic; with the assert
compiled out, `lockmgr` on freed module data is a UAF write.

## 3. Live verification (DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026, X86_64_GENERIC)

1. **`usb_quirk` is compiled into the GENERIC kernel** —
   `sys/conf/files:3028` (`bus/u4b/quirk/usb_quirk.c optional usb`), and live:
   `kldload usb_quirk` → "module already loaded or in kernel" (run.2.log);
   an unprivileged GET returns live quirk-table data (vid=0x0b05). On this
   default config the quirk unload hooks run at **shutdown only**.
2. **`usb_template` IS live-loadable/unloadable** —
   `sys/conf/files:3032` (`optional usb_template`). Decisive measurement
   (run.log): 12/12 `kldunload usb_template` iterations took **exactly 1s**
   each — that wall-clock second is the `tsleep(...,hz)` grace period itself,
   measured. Nothing else in an unload path costs a fixed second.
3. **Unprivileged consumer call-rate established**: as uid=1001 via
   `/dev/usbctl` (crw-r--r-- root:operator), `USB_DEV_QUIRK_GET` dispatch ran
   156,989 times in ~1-2s (~10^5/s) — on modular-quirk builds this is the
   unprivileged side of the two-party race (the privileged side being
   `kldunload usb_quirk`).
4. **Corruption not manifested**: no panic across 12 load/unload cycles plus
   baseline runs; guest stayed up (`uptime` clean, dmesg clean). Reason: the
   victim must be delayed >1s between pointer read and call during a
   root-initiated unload — not forceable by an unprivileged user, and on this
   guest the only live-unloadable module (`usb_template`) has no host-side
   consumer that fires on this event path (its host-reachable consumer
   `usb_temp_unsetup_p` fires only during bus/device teardown; the gadget
   sites are dead code without device-mode controller hardware). Racing
   `kldunload usb_template` against `kldunload xhci` (root vs root) was
   considered and skipped: nanosecond window, negligible hit probability, and
   cycling the controller module risks wedging the guest for no evidentiary
   gain. This is an honest not_reproduced, not a refutation: the defect is
   structural and its grace period is proven live.

## 4. Negative results (classes hunted and killed in this file)

- `usb_devclass_ptr` NULL-deref: every consumer is guarded —
  `devclass_get_device` NULL-checks (`sys/kern/subr_bus.c:950-958`);
  `usb_ref_device` (`usb_dev.c:201-204`), `usb_destroy_dev`
  (`usb_device.c:2029-2035`), `usb_needs_explore_all` (`usb_hub.c:2301-2306`).
- The concurrent first-writer poke `usb_devclass_ptr = devclass_find("usbus")`
  (`usb_controller.c:813-814`) is benign: idempotent same-value store.
- Stub info leak: none — `usb_dynamic.c:67-99` stubs return constants; the
  unsetup stub frees+NULLs (no double-free; core allocator).
- Pointer tearing/missing barriers: benign on x86_64 (aligned pointer stores).
- Missing privilege checks in dispatch: none reachable — quirk ADD/REMOVE are
  `caps_priv_check_self(SYSCAP_NODRIVER)`-gated inside `usb_quirk_ioctl`;
  GET/NAME_GET expose only the static quirk table (public data).
- `usb_bus_unload` sets `usb_devclass_ptr = NULL` — in-kernel core
  (`sys/conf/files:2865`), so shutdown-only on any `device usb` kernel; with
  NULL-safe consumers this is not triggerable into a deref.
- Cosmetic: `usb_temp_unload`/`usb_bus_unload` sleep on the wrong ident
  (`usb_quirk_unload`, usb_dynamic.c:129/143) — no wakeup caller exists.
- DF-5394 family (usb_controller.c attach race) not re-reported here.

## 5. Fix

`fix.diff` (git apply --check clean, never applied to `sys/`): one global
`struct lock usb_dyn_lock`; consumers wrap pointer-read **plus** indirect call
in `USB_DYN_RLOCK()/USB_DYN_RUNLOCK()` (shared); the three unload hooks take
the lock EXCLUSIVE around the pointer resets and drop the tsleep entirely —
exclusive acquisition proves no CPU is executing inside the module when the
hook returns. The gadget-side dispatch in `usb_handle_request.c:644` is
deliberately NOT wrapped: it has no in-tree caller and would run from callback
context holding the BUS lock, which would introduce a BUS→dyn vs dyn→BUS
inversion against `usb_request.c`; whoever wires that callback must acquire
dyn before BUS at their boundary. Fix validation (patched-kernel rebuild) was
not performed: the finding is Low / root-gated, the corruption itself never
reproduced, so there is no baseline bad behavior to observe disappearing —
fix_status not_testable, honestly.

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