usbd_get_phys() type-confuses the USB controller's newbus ivars as struct pci_devinfo * without any bus-type or NULL check β NULL/wild deref panic or limited kernel-heap leak into the V4L2 bus_info string on non-PCI controllers
Summary
device_get_ivars(device_get_parent(bdev)) dereferenced as pci_devinfo with no devclass/NULL check: on non-PCI host controllers (embedded MUSB/OTG in-tree) NULL/wild deref panic at UVC attach, or a small kernel-derived string disclosed via UVC VIDIOC_QUERYCAP. Unreachable on x86 (all HCI PCI). Distinct from DF-1005 (same fn, strlen underflow). Surrounding code soak-verified on the guest rig: 139,053 hostile ugen ioctls + 48 replug cycles on live xhci+ehci, zero panics. devclass/NULL-check + bus-unit fallback fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5299 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 7.6 KB | β raw | |
| usbsoak.c | β | 4.9 KB | view raw | |
| build.sh | β | 91 B | view raw | |
| run.sh | β | 156 B | view raw | |
| build.log | β | 128 B | view raw | |
| run.log | β | 560 B | view raw | |
| run.2.log | β | 386 B | view raw | |
| env.txt | β | 439 B | view raw | |
| fix.diff | β | 874 B | view raw | |
| verdict.json | β | 2.8 KB | view raw | |
| manifest.json | β | 857 B | view raw |
DF-5299 evidence pack β usb_device.c pass-2
What this pack contains
-
The finding DF-5299 (
usbd_get_phys()uncheckedpci_devinfo *type confusion / NULL parent deref) β source-verified only: the trigger requires a USB host controller whose newbus parent is NOT a PCI device (or has no ivars). On the x86-64 QEMU target every USB HCI (xhci, ehci) is a PCI child, so the faulty branch cannot be entered at runtime here. That is an architectural limit of the test target, not a refutation. -
A file-level stress soak of
sys/bus/u4b/usb_device.c(usbsoak.c) β the primary artifact of this pack. It hammers every ugen-reachable entry point into the audited file with hostile-but-legal values while USB devices are replugged underneath, to give the pass-2 negative result (no new memory-safety defect beyond DF-1003/1004/1005) runtime weight.
Build (on the guest, as root)
cc -O2 -pthread -o usbsoak usbsoak.c
Run (as root on the guest)
# guest booted with USB devices, e.g. QEMU: # -device qemu-xhci,id=xhci0 \ # -device usb-ehci,id=ehci0 \ # -drive if=none,id=usb0,file=usbstick.img,format=raw \ # -device usb-storage,bus=ehci0.0,drive=usb0 \ # -device usb-tablet,bus=ehci0.0,port=2,id=tab0 ./usbsoak 424242 # ~90 s, two devices + reopener thread # concurrently, from the QEMU monitor: # device_del tab0 ; device_add usb-tablet,bus=ehci0.0,port=2,id=tab0 # (48 total cycles, 18 slow ~2.4 s + 30 fast)
Expected output (observed)
soak done: ops=70139 alt_ok=56 cfg_ok=939 (errors expected, panic = FAIL) soak done: ops=68914 alt_ok=34 cfg_ok=620 (errors expected, panic = FAIL)
- 139,053 hostile ioctls across USB_SET_ALTINTERFACE (iface/alt values including 31/255 + random), USB_SET_CONFIG (0/1/2/255/random), USB_GET_FULL_DESC (random config index/offset/len), USB_DO_REQUEST, USB_IFACE_DRIVER_DETACH.
- 1,559 successful set_config cycles = 1,559 full
usb_unconfigureβusb_config_parse(ALLOC|INIT|FREE)βusb_cdev_createround-trips, concurrent with open/ioctl on the same device. - 90 successful alt changes plus tens of thousands of IN_USE/EINVAL rejections
through the
usb_config_parse()zeroing loop. - 48 device replug cycles concurrent with the above (usb_free_device / usb_alloc_device / async cdev destroy churn, device-index reuse window).
Result: no panic, no devfs corruption warnings, guest stayed up and
consistent after both runs. The usb_device.c state machine survived
everything thrown at it.
Guest
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC), QEMU/KVM, qemu-xhci + usb-ehci + usb-storage + usb-tablet
DF-5299 itself: inconclusive at runtime β trigger needs non-PCI USB HCI (absent on x86); see VERDICT.md.
VERDICT β DF-5299 (and the usb_device.c pass-2 soak)
Finding DF-5299 β usbd_get_phys() type-confused pci_devinfo * deref
Status: inconclusive (runtime), certain (source). Severity Low, confidence speculative (impact), certain (defect exists). Not reproduced β the trigger is architecturally absent on the x86-64 test target.
Root cause (source-verified)
sys/bus/u4b/usb_device.c:2249-2258:
struct pci_devinfo *info;
...
info = device_get_ivars(device_get_parent(udev->bus->bdev));
ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
info->cfg.domain, info->cfg.bus,
info->cfg.slot, info->cfg.func);
The USB bus device's (usbusN) parent is the host-controller device_t.
The code assumes that controller is a PCI child and dereferences its
ivars as struct pci_devinfo * with no bus-type check and no NULL
check:
- if the controller's parent provides no ivars (or
device_get_parent()returns NULL β controller on the root nexus),infois NULL/garbage andinfo->cfgis a NULL/wild deref β kernel panic at attach of the first UVC device on that bus; - if the controller hangs off any non-PCI bus whose ivars are a different
struct, four attacker-irrelevant but kernel-controlled words of adjacent
heap are formatted into
phys, which is copied to userspace bysys/bus/u4b/uvc/uvc_v4l2.c:132βVIDIOC_QUERYCAP.bus_infoβ a limited kernel info leak.
Sole caller: uvc_v4l2.c:132 (UVC attach / QUERYCAP). On DragonFly x86-64
every compiled HCI (uhci/ehci/xhci/ohci) is a PCI device, so the branch is
unreachable there; it is live on embedded targets (AVR32/AT91/MUSB/Octeon
OTG controllers in sys/bus/u4b/controller/) and for any future non-PCI
HCI (e.g. ACPI-described MMIO controllers).
Why not runtime-verified here
The QEMU guest exposes USB only via qemu-xhci/usb-ehci, both PCI. A UVC
device is additionally required (QEMU does not emulate one; usbd_get_phys
is UVC-only). Neither precondition can be constructed on this rig, so no
runtime verdict is possible β inconclusive, per the honest-status rules.
Recommended fix
Guard the deref: only format the PCI tuple when the grandparent is a PCI
bus (or use pci_get_<domain/bus/slot/func> style accessors after a type
check), else fall back to a bus-unit-only string:
--- a/sys/bus/u4b/usb_device.c
+++ b/sys/bus/u4b/usb_device.c
@@ -2246,7 +2246,11 @@ usbd_get_phys(struct usb_device *udev, char *phys, uint8_t len)
if (!phys)
return;
- info = device_get_ivars(device_get_parent(udev->bus->bdev));
- ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
- info->cfg.domain, info->cfg.bus,
- info->cfg.slot, info->cfg.func);
+ info = device_get_ivars(device_get_parent(udev->bus->bdev));
+ if (info != NULL && device_get_devclass(device_get_parent(
+ udev->bus->bdev)) == devclass_find("pci"))
+ ksnprintf(phys, len, "usb-%02x:%02x:%02x.%x-",
+ info->cfg.domain, info->cfg.bus,
+ info->cfg.slot, info->cfg.func);
+ else
+ ksnprintf(phys, len, "usb-%u-", device_get_unit(udev->bus->bdev));
The usb_device.c pass-2 soak (file-level negative-result evidence)
Rig: QEMU/KVM DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, build of
2026-07-02) with qemu-xhci (usbus0, root hub only β every downstream
device failed enumeration, exercising the usb_alloc_device error path)
and usb-ehci (usbus1) with usb-storage (ugen1.2) and usb-tablet
(ugen1.3).
usbsoak.c(this pack): 3 threads Γ 2Γ90 s β USB_SET_ALTINTERFACE/SET_CONFIG/GET_FULL_DESC/DO_REQUEST/ IFACE_DRIVER_DETACH with values including 31/255/random on/dev/usb/1.2.0and/dev/usb/1.3.0, plus a reopener loop on/dev/usb/1.3.1.- Concurrent QEMU-monitor replug storm: 18 slow (~2.4 s gap) + 30 rapid
device_del tab0/device_add usb-tablet,...cycles (device-index reuse window:usb_free_deviceβ async cdev destroy βusb_alloc_deviceβusb_make_devsame names).
Observed totals: 139,053 ioctls, 1,559 successful full
unconfigure/configure cycles, 90 successful alt changes, 48 replugs; zero
panics, zero devfs duplicate-name warnings ( DragonFly devfs refuses
duplicates gracefully β devfs_core.c:2111-2117 β the collision window is
functional-only), guest healthy after both runs.
Classes hunted and killed in this pass (path:line)
- Descriptor fetch/cache:
usbd_req_get_desclength negotiation is monotonic-shrink, all writes bounded by caller max_len (usb_request.c:1043-1070);usbd_req_get_config_desc_fullforceswTotalLength = lenafter the kmalloc (usb_request.c:1358-1376); langid parse boundedscratch_ptr[0] β€ 4(usb_device.c:1785-1819); string parsen β€ temp[0]/2-1 β€ 126 < 255scratch (usb_request.c:1127-1182, usb_device.h:178). usb_config_parsetables: endpoint slot index checkedtemp < USB_MAX_EP_UNITS(32)beforeudev->endpoints + temp(usb_device.c:802-807);ep_max β€ 32== alloc size (usb_device.c:845); iface count boundedUSB_IFACE_MAX(32)(usb_parse.c:156); ALLOC/INIT re-parse deterministic over same cdesc. The zero-before-verify defect (incl. its "IN_USE continues zeroing" variant) is DF-1004 β not re-reported.- State machine teardown:
endpoints_max/ep_currmutated only under USB_BUS_LOCK (usb_device.c:855-871) and re-read per invocation byusb_do_clear_stall_callback(usb_request.c:234-332) β no cross-lock stale-pointer UAF;usb_free_deviceorder (state=DETACHED under usb_ref_lock at 2150/2832 before any ref taker,usb_wait_pending_refssets REF_MAX under the lock at 462-473) closes the ref race;usbd_transfer_unsetupof ctrl_xfer precedes final free withusb_proc_mwait(cs_msg)(2192-2195). - Refcount/devfs lifecycle:
usb_ref_devicere-resolvesbus->devices[dev_index]underusb_ref_lockwith DETACHED gate (usb_dev.c:200-225); pd stores indices, not udev pointers (usb_device.c:1978-1982) β replug cannot produce a stale udev deref; fast-replug cdev name collision resolves as devfs "already exists, ignored" (functional only). - Peer/hub relations: depth capped β€ 0x10 before alloc (1575);
device_index scan bounded by
devices_max(1564-1573);hs_hubwalk bounded by depth (1642-1654);usbd_get_physno[] walk bounded by depth β€ 16 < 128 (2260-2264). - Quirk matching:
usb_test_quirkbounded scan ofautoQuirk[](2596-2617);usbd_add_dynamic_quirkbounded (2988-3001). - Do-request sizes: all
usbd_do_request*from this file pass stack/scratch buffers sized β₯ wLength; timeout clamped 50..30000 ms (usb_request.c:421-428). - ugen open/ioctl interplay: every path takes the udev ref + enum lock
(usb_dev.c:231), serializing against
usb_unconfigure/usb_free_device;ugen_set_configis deliberately async (usb_generic.c:614-624) to avoid the fifo-drain self-deadlock; SET_ALT skips EP0 fifos (usb_device.c:2781-2785).
Companion finding DF-5300 (Info, no runtime aspect)
usbd_get_iface() computes udev->ifaces + iface_index before the
iface_index >= udev->ifaces_max bounds check (usb_device.c:2237-2244).
Never dereferenced when OOB, but the OOB (and NULL-base) pointer
arithmetic executes on every call with an unvalidated index β C UB and a
loaded footgun for future callers. Hardening: move the addition after the
check. No PoC (not a runtime defect).
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.
Fix verification
not_testableFix not buildable/testable: the buggy branch cannot be entered on the PCI-only x86 target, so no baseline behavior exists to compare against. Diff authored against the read-only sys/ tree only.
VERDICT.md contains the git-apply-able fix.diff content; never applied to sys/.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (root cause + kill list), usbsoak.c + run.log + run.2.log (soak: ops=70139/68914, no panic), env.txt (rig). Trigger needs non-PCI USB HCI + UVC device: absent on this target.
PoC changes
n/a β finding not runtime-triggerable on the PCI-only x86 rig; the soak harness was authored fresh (no seed PoC existed).
Verified recommended fix
Type-check/NULL-check the controller ivars in usbd_get_phys() before formatting the PCI tuple; fall back to a bus-unit-only prefix otherwise (diff in VERDICT.md).
Verdict
Source-verified defect: usbd_get_phys() (sys/bus/u4b/usb_device.c:2249-2258) casts the USB host controller's newbus ivars to struct pci_devinfo * with no bus-type or NULL check; on a non-PCI HCI (or one with no ivars) this is a NULL/wild deref -> panic at UVC attach, or a limited kernel-heap info leak into the V4L2 bus_info string copied to userspace (uvc_v4l2.c:132). Runtime verification is architecturally impossible on the x86-64 QEMU target (all HCIs are PCI; no UVC emulation), hence inconclusive rather than not_reproduced. The accompanying file-level soak (139,053 hostile ugen ioctls, 1,559 full config re-select cycles through usb_config_parse ALLOC/INIT/FREE + usb_cdev_create, 90 alt changes, 48 concurrent device replug cycles) produced no panic or corruption, weighting the pass-2 negative result for the rest of usb_device.c.
No comments yet.