β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5299

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5299 Β· 12 files
FileTypeDescriptionSize
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

  1. The finding DF-5299 (usbd_get_phys() unchecked pci_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.

  2. 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_create round-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.md
↓ download raw

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), info is NULL/garbage and info->cfg is 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 by sys/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.

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.0 and /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_dev same 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_desc length negotiation is monotonic-shrink, all writes bounded by caller max_len (usb_request.c:1043-1070); usbd_req_get_config_desc_full forces wTotalLength = len after the kmalloc (usb_request.c:1358-1376); langid parse bounded scratch_ptr[0] ≀ 4 (usb_device.c:1785-1819); string parse n ≀ temp[0]/2-1 ≀ 126 < 255 scratch (usb_request.c:1127-1182, usb_device.h:178).
  • usb_config_parse tables: endpoint slot index checked temp < USB_MAX_EP_UNITS(32) before udev->endpoints + temp (usb_device.c:802-807); ep_max ≀ 32 == alloc size (usb_device.c:845); iface count bounded USB_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_curr mutated only under USB_BUS_LOCK (usb_device.c:855-871) and re-read per invocation by usb_do_clear_stall_callback (usb_request.c:234-332) β€” no cross-lock stale-pointer UAF; usb_free_device order (state=DETACHED under usb_ref_lock at 2150/2832 before any ref taker, usb_wait_pending_refs sets REF_MAX under the lock at 462-473) closes the ref race; usbd_transfer_unsetup of ctrl_xfer precedes final free with usb_proc_mwait(cs_msg) (2192-2195).
  • Refcount/devfs lifecycle: usb_ref_device re-resolves bus->devices[dev_index] under usb_ref_lock with 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_hub walk bounded by depth (1642-1654); usbd_get_phys no[] walk bounded by depth ≀ 16 < 128 (2260-2264).
  • Quirk matching: usb_test_quirk bounded scan of autoQuirk[] (2596-2617); usbd_add_dynamic_quirk bounded (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_config is 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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/.
↓ fix.diffper-fix-DF-5299

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.