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)
PoC verification
Evidence pack
findings/poc/DF-5301 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.%locationwhereuhid0's parent is the detachinguhub1.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-> guestuhub1device_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.%locationprintsbus=0 hubaddr=2 port=1 devaddr=3 interface=0 ugen=ugen0.3β served by uhub1'suhub_child_location_string.
Files
hammer.cβ unprivileged reader storm (threads hammering the vulnerable sysctls + the global walk), built/run as regular usermaxx.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-guardhubin both methods), never applied to the reposys/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.
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'suhub_child_location_stringβ world-readable.hw.bus.devicesglobal 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_addof hub+tablets β 953 fulluhub_detachwindows generated (detach_count.txt), each producing thekfree(hub)/hub=NULLtransition with the device stillDS_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)
- OID lifecycle kills fresh lookups at the window. The children
(
uhid0/uhid1) are torn down inuhub_detach()'s port loop (:1567-1579) beforekfree(hub). Their sysctl OIDs are removed (device_sysctl_fini inside device_detach of each child) ~1-2 ms beforehub=NULL. At the vulnerable instant (after :1592) fresh sysctl lookups return ENOENT β dispatch rate to the vulnerable method is zero exactly whenhubis NULL. - 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=NULLstore executing on another CPU β measured/estimated rate β1e-7/s. (Theoid_runningdrain insysctl_remove_oid_lockedthat would have serialized this cleanly β kern_sysctl.c:387-397 β is dead code in this tree: the counter is never incremented.) - The global walk re-derives pointers after every sleep.
sysctl_deviceswalks the unlockedbus_data_devicesTAILQ and calls the methods per device, but its only blocking points (SYSCTL_OUT copyouts) lie between devices; after each copyout the iterator reloadsTAILQ_NEXTanddevremoved()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_testableFix 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)
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.
No comments yet.