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

TOCTOU: uhub_child_location_string()/uhub_child_pnpinfo_string() dereference sc->sc_udev->hub after a device_is_attached() guard that stays true while uhub_detach() frees and NULLs it (NULL-deref panic / UAF read)

Summary

device_is_attached stays TRUE during the whole uhub_detach body (DS_NOTPRESENT set only after DEVICE_DETACH returns + tail) while hub is kfree'd and NULLed inside - the check->deref interval has no blocking point on this kernel, so the interleaving collapses to ~ns interrupt alignment. Extensive honest campaign: 953 detach windows vs 60-thread unpriv reader storm at ~155-190k dispatches/s, 4 campaigns, NO panic - not_reproduced with full mechanics traced (OID removal ~1-2ms before NULL, non-preemptible interval, hw.bus.devices re-derivation, oid_running drain dead code). Real missing synchronization; becomes an unpriv panic the moment any sleep lands in the interval. NULL-guard-both-methods fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5301 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 6.1 KB ↓ raw
hammer.c β€” 1.9 KB view raw
replug.py β€” 1.4 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 246 B view raw
fix.diff β€” 588 B view raw
verdict.json β€” 4.3 KB view raw
manifest.json β€” 1.6 KB view raw
detach_count.txt β€” 4 B view raw
hammer-tail.log β€” 1.3 KB view raw
boot-snippet.txt β€” 1.7 KB view raw
replug3.log β€” 398 B view raw
replug4.log β€” 298 B view raw
env.txt β€” 238 B view raw

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.

VERDICT.md
↓ download raw

DF-5301 VERDICT β€” uhub_child_location_string/pnpinfo_string TOCTOU

Status: not_reproduced (code defect certain; exploitable interleaving not closed on this kernel after an extensive, instrumented campaign).

The defect (source-certain)

uhub_child_location_string() (sys/bus/u4b/usb_hub.c:1660) and uhub_child_pnpinfo_string() (:1702) do:

if (!device_is_attached(parent)) { ... return; }   /* usb_hub.c:1668 */
sc  = device_get_softc(parent);                    /* :1674 */
hub = sc->sc_udev->hub;                            /* :1675 */
uhub_find_iface_index(hub, child, &res);           /* :1677 -> :1637 nports = hub->nports */

device_is_attached() is dev->state >= DS_ATTACHED (sys/kern/subr_bus.c:1902-1905). In DFly's device_detach() (sys/kern/subr_bus.c:2129-2153) the state stays DS_ATTACHED for the whole duration of DEVICE_DETACH() and until after device_printf(), devremoved(), devclass_delete_device() β€” i.e. the guard returns TRUE during the entire body of uhub_detach() and for a tail of ~100-300 Β΅s after it. But uhub_detach():

kfree(hub, M_USBDEV);          /* usb_hub.c:1590 β€” hub freed */
sc->sc_udev->hub = NULL;       /* usb_hub.c:1592 β€” hub NULLed */

inside that window. A reader between the check and the deref gets either a freed struct usb_hub * (UAF read; hub->ports, hub->hubudev at :1639-1640) or NULL (kernel NULL-deref panic at :1637). Additionally usb_bus_port_get_device() (sys/bus/u4b/usb_hub.c:2190-2201) reads bus->devices[up->device_index] without usb_ref_lock and without a reference, so the port walk inside uhub_find_iface_index() can chase a usb_device that usb_free_device() (sys/bus/u4b/usb_device.c:2138-2224) has cleared and kfree'd, feeding freed-heap-derived values into the user-readable location/pnpinfo strings.

Entry points (verified live on the guest):

  • dev.uhid.0.%location β†’ bus=0 hubaddr=2 port=1 devaddr=3 interface=0 ugen=ugen0.3 β€” served by uhub1's uhub_child_location_string β€” world-readable.
  • hw.bus.devices global walk (sys/kern/subr_bus.c:3873-3905) calls both methods for every device β€” world-readable.

Reproduction attempt (extensive)

Environment: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (Jul 2 2026), 6 vCPU KVM, QEMU -device piix4-usb-uhci,id=uhci0 with usb-hub on root port 1 (uhub1, 8 ports) and two usb-tablet children on ports 1.1/1.2 (uhid0, uhid1 β€” children of uhub1).

  • Reader storm as unprivileged user (maxx, no groups): 60 threads tight-looping the four vulnerable sysctls plus a global walk thread β€” sustained ~155,000-190,000 successful dispatches/s while the hub exists (hammer-tail.log).
  • Host-side replug loop (QEMU monitor): device_del h1 / re-device_add of hub+tablets β€” 953 full uhub_detach windows generated (detach_count.txt), each producing the kfree(hub)/hub=NULL transition with the device still DS_ATTACHED.
  • Four campaigns (replug3/replug4 logs): 400 + 300 cycles at full load, plus ~350 earlier cycles with a diluted hammer.

Result: no panic, no fault, guest stayed up through every window.

Why the interleaving does not close (traced)

  1. OID lifecycle kills fresh lookups at the window. The children (uhid0/uhid1) are torn down in uhub_detach()'s port loop (:1567-1579) before kfree(hub). Their sysctl OIDs are removed (device_sysctl_fini inside device_detach of each child) ~1-2 ms before hub=NULL. At the vulnerable instant (after :1592) fresh sysctl lookups return ENOENT β€” dispatch rate to the vulnerable method is zero exactly when hub is NULL.
  2. In-flight readers cannot be stalled in the checkβ†’deref interval. The interval is ~10-50 ns of straight-line code with no blocking call and no lock. DFly LWKT does not preempt a kernel-resident thread for an equal-priority (user) thread; only an interrupt can delay it, and the delay window (~ns-Β΅s) must contain the single hub=NULL store executing on another CPU β€” measured/estimated rate β‰ˆ1e-7/s. (The oid_running drain in sysctl_remove_oid_locked that would have serialized this cleanly β€” kern_sysctl.c:387-397 β€” is dead code in this tree: the counter is never incremented.)
  3. The global walk re-derives pointers after every sleep. sysctl_devices walks the unlocked bus_data_devices TAILQ and calls the methods per device, but its only blocking points (SYSCTL_OUT copyouts) lie between devices; after each copyout the iterator reloads TAILQ_NEXT and devremoved() has already unlinked the detaching children, so post-stall iterations skip them.

The residual reachable window is thus interrupt/SMP alignment of a single store inside a ~10 ns instruction gap β€” not demonstrable. (Under a kernel change that inserts any blocking point between device_is_attached() and the hub load β€” or a reader that holds the values longer β€” the defect becomes a NULL-deref panic/UAF-read.)

Fix

fix.diff (applies clean against sys/bus/u4b/usb_hub.c): NULL-guard hub after the device_is_attached() check in both methods. This converts any future interleaving into a benign empty string. Fix validation (patched-kernel build + re-run) was not performed: the baseline race never reproduced, so there is no observable to diff against; the guard is a trivially-correct hardening (fix_status: not_testable).

Bottom line

Real missing-synchronization defect (CWE-367 + CWE-476/CWE-416 pattern), certain in source, reachable only through sysctl reader paths that are world-readable, but the exploitable interleaving window collapses to ~nothing on this kernel because of the sysctl OID lifecycle and DFly's non-preemptible straight-line kernel intervals. Filed as hardening with a one-line-per-callsite fix. No impact demonstrated; not a practical DoS on this system as built.

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 built: baseline never reproduced, no observable to diff; the NULL-guard is trivially-correct hardening (git apply --check passes against sys/bus/u4b/usb_hub.c).

findings/poc/DF-5301/fix.diff (git-apply clean, +12 lines in 2 hunks)
↓ fix.diffper-fix-DF-5301

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5301/VERDICT.md (full mechanics); detach_count.txt (953 windows); hammer-tail.log (~155-190k dispatches/s sustained, zero faults); replug3.log/replug4.log (400+300 cycles); boot-snippet.txt (uhub1/uhid attach-detach churn); hammer.c/replug.py (exact harness); fix.diff (NULL guard, applies clean)

PoC changes

Rewrote the seeded concept into hammer.c v3 (tight loop only on the 4 sysctls that dispatch uhub1's methods, after measuring that dev.uhub.1.* dispatches the root-hub method and is useless as a target) + host-side monitor replug driver (replug.py); switched QEMU xhci->piix4-usb-uhci because DFly xhci hotplug enumeration of qemu-xhci devices fails before probing uhub.

Verified recommended fix

NULL-guard hub after device_is_attached() in both uhub_child_location_string() and uhub_child_pnpinfo_string() (see fix.diff)

Verdict

Code defect certain, exploitable interleaving not closed. uhub_child_location_string()/uhub_child_pnpinfo_string() (sys/bus/u4b/usb_hub.c:1660-1757) guard a sc->sc_udev->hub dereference with a non-atomic device_is_attached(parent) check; device_is_attached stays TRUE for the whole of uhub_detach() and ~100-300us of device_detach() tail (sys/kern/subr_bus.c:2129-2153), while uhub_detach kfrees the hub (usb_hub.c:1590) and NULLs it (:1592) inside that window -> NULL-deref panic at usb_hub.c:1637 or UAF read; usb_bus_port_get_device (:2190-2201) additionally reads bus->devices[] unlocked. Reachable only via world-readable sysctls (dev.uhid.N.%location/%pnpinfo, hw.bus.devices walk). Campaign on the QEMU guest: 953 full uhub_detach windows (QEMU usb-hub device_del cycling) against an unprivileged 60-thread reader storm sustaining ~155-190k successful vulnerable dispatches/s - zero panics. Traced cause: child sysctl OIDs are removed ~1-2ms BEFORE hub=NULL so no fresh dispatches exist at the vulnerable instant; the ~10-50ns check->deref interval contains no blocking/preemption point (DFly does not preempt kernel-resident equal-priority threads); the global walk re-derives device pointers after each copyout sleep and skips unlinked children. Residual window is single-store/ns-gap interrupt alignment (~1e-7/s) - not demonstrable. The oid_running drain that would serialize this (kern_sysctl.c:387-397) is dead code (counter never incremented). Filed as hardening with NULL-guard fix; practical impact none on this kernel as built.