ums_detach tears down the USB fifo and evdev before stopping/draining the intr xfer: with an evdev consumer holding the device, a report delivered during detach dereferences a NULL/freed usb_fifo or the freed evdev -> kernel panic (Fault VA 0x0)
Summary
Teardown order is fifo->evdev->unsetup, but with an evdev holder and no /dev/umsX fifo open, usb_fifo_close early-returns (curr_cpd==NULL) so the fifo teardown never stops the xfer - fp[RX] NULLed and sc_evdev kfree'd while the callback still runs: put_bytes_max(NULL) reads VA 0, or push on the freed evdev lockmgr's its zeroed ev_state_lock. REPRODUCED on the stock module (QEMU usb-mouse + root evdev holder + ~90 reports/s + kldunload-under-stream): Fatal trap 12 VA 0x0 at lockmgr_exclusive+0x5d, guest dead at ~cycle 12; instrumented build proves the window; reorder fix validated 150/150 cycles clean. DF-5324 family, evdev flavor. Stop-and-drain-first fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5368 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 5.7 KB | β raw | |
| umsrace.py | β | 5.0 KB | view raw | |
| race_v2.py | β | 2.2 KB | view raw | |
| holdopen.c | β | 557 B | view raw | |
| build.sh | β | 123 B | view raw | |
| run.sh | β | 201 B | view raw | |
| build.log | β | 417 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| bootpanic.log | β | 81.1 KB | view raw | |
| panic.txt | β | 735 B | view raw | |
| env.txt | β | 644 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_run.log | β | 87 B | view raw | |
| sha256sums.txt | β | 231 B | view raw |
DF-5368 β ums_detach teardown-order NULL/UAF panic (evdev consumer + driver detach under load)
What
ums_detach() tears down the USB fifo and the evdev device BEFORE the
interrupt transfer is stopped and drained. When the intr xfer is running
with NO /dev/umsX fifo open (exactly the state an evdev client β X11,
libinput, moused alternatives β creates via ums_ev_open()), the FIFO
teardown does not stop the transfer at all: usb_fifo_close() returns
early (usb_dev.c:779, curr_cpd == NULL) and usb_fifo_detach()
NULLs sc->sc_fifo.fp[USB_FIFO_RX] (usb_dev.c:2032) while the xfer is
still live. evdev_free() then kfrees sc->sc_evdev (ums.c:776) β
still before usbd_transfer_unsetup() (ums.c:780) stops and drains
the callback.
Any interrupt report completing inside that window runs
ums_intr_callback() against a NULL/freed world:
ums.c:371usb_fifo_put_bytes_max(sc->sc_fifo.fp[USB_FIFO_RX])βf == NULL,free_qis at offset 0 ofstruct usb_fifo(usb_dev.h:103) β read of VA 0x0.ums.c:361βums.c:922usb_fifo_put_data_linear(NULL, ...)βUSB_IF_DEQUEUE(&f->free_q)β same NULL read.ums.c:363βums_evdev_push()on a freed/zeroedsc->sc_evdevβEVDEV_ENTERβlockmgr(NULL-derived ev_state_lock).
Observed crash (stock module): Fatal trap 12, fault VA 0x0,
supervisor READ, PC lockmgr_exclusive+0x5d (orl (%r14),%esi,
r14 = 0) β the evdev flavor of the same window. All flavors share the
root cause and are closed by the same fix.
Rig
Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (EVDEV_SUPPORT, evdev
device), QEMU/KVM, -device piix3-usb-uhci -device usb-mouse,id=m1
(QEMU's HID mouse attaches ums0: "5 buttons and [XYZ] coordinates").
Build
# on the guest, as root: cc -O2 -o /root/holdopen holdopen.c
Run (the panic)
python3 race_v2.py 150
Each cycle: kldload stock ums.ko (ums0 attaches to the live mouse),
root /root/holdopen holds the ums0 evdev node open (starts the intr
xfer with no fifo open), the host spams mouse_move via the QEMU
monitor socket (~90 reports/s), then kldunload ums detaches the driver
while the device keeps delivering.
Expected
Kernel panic within tens of cycles:
Fatal user address access from kernel mode from holdopen at ffffffff80647bad Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 fault code = supervisor read data, page not present instruction pointer = 0x8:0xffffffff80647bad (lockmgr_exclusive+0x5d) ... Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi
Observed: guest down at cycle ~12 (run.log + bootpanic.log).
Fix validation
fix.diff reorders ums_detach(): usbd_transfer_unsetup() +
usb_callout_drain() FIRST, then usb_fifo_detach(), then
evdev_free(). usbd_transfer_stop() is NULL-safe
(usb_transfer.c), so the later fifo/evdev close paths
(ums_stop_rx) remain correct.
With the patched module: 150/150 identical cycles clean, guest up,
plus the moused-held /dev/ums0 fifo-open detach under stream clean
every cycle (fix_run.log).
DF-5368 VERDICT β REPRODUCED (kernel panic, NULL-deref/UAF in ums detach window)
Result
| item | value |
|---|---|
| status | reproduced |
| impact | panic (kernel NULL-deref / use-after-free window; DoS) |
| baseline | stock /boot/kernel/ums.ko, guest panicked at cycle ~12 |
| patched | fix.diff (detach reorder), 150/150 cycles clean |
| confidence | certain |
How it was reproduced
Trigger: driver detach while the device still streams interrupt-IN
reports and an evdev client (not a /dev/umsX fifo) holds the device
open.
- QEMU usb-mouse attached to piix3-usb-uhci; ums0 attaches ("5 buttons and [XYZ] coordinates ID=0").
- Root process (
holdopen) holds ums0's evdev node (/dev/input/eventN, found viakern.evdev.input.N.name) open. Open runsevdev_register_clientβums_ev_open(ums.c:964-971) βums_start_rxβ the intr xfer runs withsc->sc_fifonever opened. - Host-side monitor spam:
mouse_moveevery 2 ms β ~90 reports/s complete; each completion runsums_intr_callback(verified earlier by reading a stream of REL events off the node). kldunload ums(software driver detach; the device stays attached and streaming β this is the only in-guest detach that keeps the report source alive; noteusbconfig set_configon this stack never detaches the driver, and QEMUdevice_delkills deliveries ~250 ms before the driver detach runs, so neither works as a trigger).- ~12 cycles in:
Fatal trap 12, fault VA 0x0, PClockmgr_exclusive+0x5d(resolved against/boot/kernel/kernelsymbols: lockmgr_exclusive = 0xffffffff80647b50; the faulting address 0xffffffff80647bad = +0x5d).
Why the PC is lockmgr and not usb_fifo_put_bytes_max: with
free_q at offset 0 of struct usb_fifo (usb_dev.h:102-104), a
NULL-fifo read would fault in usb_fifo_put_bytes_max/put_data_linear; the observed lockmgr(NULL) instead matches the
SAME window's other sink β ums_evdev_push() (ums.c:363) on
sc->sc_evdev freed at ums.c:776 (before the xfer drain at :780):
the freed struct evdev_dev gets its ev_state_lock field zeroed by
memory reuse, and EVDEV_ENTER does lockmgr(ev_state_lock) β
lockmgr_exclusive(0) β read at VA 0x0. A callback that passes
ums_put_queue (line 361) before the fifo pointer is NULLed reaches
exactly this.
Proof of the window (instrumented module, stock logic + kprintf)
Markers added to ums.c (built as a module on the guest) showed, with 151 events streamed before the detach:
UMSDIAG detach enter fpRX=0xfffff8008d3cd700 UMSIAG detach post-fifo fpRX=0 <- fp[RX] NULLed, xfer STILL RUNNING UMSDIAG detach pre-evdev-free UMSDIAG detach post-evdev-free UMSDIAG detach post-unsetup <- xfer stopped here
i.e. usb_fifo_detach β evdev_free β usbd_transfer_unsetup run
back-to-back with the transfer live between the first and last marker.
The Β΅s-wide gap between "post-fifo" and the stop needs only one
delivered report to crash β hit on the 12th stock cycle.
Negative controls: - 5Γ unplug cycles with nothing open (xfer never started): clean. - dd-read of the node under spam: events flow (callback proven live). - 170 physical unplug cycles: no panic β deliveries stop ~250 ms (hub poll) before ums_detach, so the window cannot be fed that way; this is an artifact of the emulation, not of the bug: real hardware / software detach paths that keep the endpoint armed (kldunload, driver rebind) keep the window live.
Root cause (path:line)
sys/bus/u4b/input/ums.c:773usb_fifo_detach(&sc->sc_fifo)firstsys/bus/u4b/usb_dev.c:779early return ofusb_fifo_closewhencurr_cpd == NULL(no fifo open) β xfer NOT stopped theresys/bus/u4b/usb_dev.c:2031-2032f_sc->fp[USB_FIFO_RX] = NULLsys/bus/u4b/input/ums.c:775-777evdev_free(sc->sc_evdev)+sc->sc_evdev = NULLβ before the drainsys/bus/u4b/input/ums.c:780usbd_transfer_unsetup(the first point the xfer is guaranteed stopped/drained)- sinks:
ums.c:371(usb_fifo_put_bytes_max(NULL)),ums.c:922(usb_fifo_put_data_linear(NULL)),ums.c:363βums.c:937(evdev_push_*on freed evdev) sys/bus/u4b/usb_dev.h:103free_qat offset 0 of usb_fifo (explains fault VA 0x0 for the fifo flavor)
Threat model
Requires an open evdev consumer on the ums device (root or group
input) plus a driver detach while reports flow: physical
unplug/re-dock on real hardware where the HC keeps the endpoint armed
briefly, kldunload ums, module rebind, or the generic USB detach path
under the right timing. The kernel crashes (DoS); the freed-evdev
flavor is a UAF read/write window into M_EVDEV memory, though we did
not develop it beyond the panic (the observable crash is the read of a
zeroed lock pointer).
Fix validation
fix.diff = reorder ums_detach (unsetup + callout drain first).
- usbd_transfer_stop(NULL) is NULL-safe (usb_transfer.c), so
ums_stop_rx reached later from usb_fifo_close/ums_ev_close
remains correct.
- Patched module, identical race: 150/150 cycles clean, guest up
(fix_run.log).
- The /dev/ums0 fifo-open detach under stream (moused holds the node
on every attach) clean across all cycles; unload RC=0.
Artifacts
- run.log β stock race, guest down at ~cycle 12 (ssh timeout)
- bootpanic.log / panic.txt β full serial-console panic
- fix.diff, fix_run.log β fix + 150/150 clean
- umsrace.py, race_v2.py, holdopen.c β rig
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
fixedPatched module (unsetup+drain first, fifo/evdev teardown after): 150/150 identical detach-under-stream cycles clean, guest up; fifo-open detach (moused holds /dev/ums0) clean every cycle. usbd_transfer_stop(NULL) is NULL-safe so late ums_stop_rx calls remain correct.
fix.diff; fix_run.log (cycles 25/50/75/100/125/150 ok, done ok=150, guest up)
Confirmed kernel references
Detail
Exploit chain
evil/mis-timed USB mouse + root evdev consumer (X11/libinput/moused alternative) + driver detach under stream (kldunload/rebind/unplug-with-armed-endpoint) -> ums_intr_callback runs against NULL/freed fifo/evdev -> kernel page fault at VA 0x0 -> panic (DoS). The freed-evdev flavor is a UAF window into M_EVDEV memory; not developed beyond the panic (crash is a read of a zeroed lock pointer).
Evidence (decisive lines)
run.log (stock race, guest down ~cycle 12), bootpanic.log + panic.txt (Fatal trap 12, VA 0x0, lockmgr_exclusive+0x5d), VERDICT.md (instrumented marker trace: detach enter fpRX=valid -> post-fifo fpRX=0 -> pre/post-evdev-free -> post-unsetup with 151 events streamed), fix.diff + fix_run.log (150/150 clean)
PoC changes
Original sketch assumed QEMU device_del could feed the window: it cannot (deliveries stop ~250ms before driver detach, hub poll interval) and usbconfig set_config never detaches the driver on this stack. Switched the detach trigger to kldunload ums with the device attached and streaming; added the evdev-node holder (holdopen) because only an evdev client starts the xfer without opening the fifo; root ssh forced through /bin/sh (root login shell is csh).
Verified recommended fix
Reorder ums_detach: usbd_transfer_unsetup() + usb_callout_drain() BEFORE usb_fifo_detach()/evdev_free() (see fix.diff).
Verdict
REPRODUCED (panic). ums_detach() tears down the USB fifo and evdev BEFORE stopping/draining the intr xfer. With an evdev client holding ums0 open (ums_ev_open started the xfer, no /dev/umsX fifo open), usb_fifo_close() early-returns (usb_dev.c:779) so usb_fifo_detach() NULLs fp[USB_FIFO_RX] (usb_dev.c:2032) and evdev_free() kfrees sc->sc_evdev (ums.c:776) while reports still complete. A report landing in that window makes ums_intr_callback() deref a NULL/freed fifo (ums.c:371/922, free_q at offset 0 of usb_fifo -> VA 0x0) or push into the freed evdev (ums.c:363 -> EVDEV_ENTER -> lockmgr(zeroed ev_state_lock)). Observed on the stock module: Fatal trap 12, fault VA 0x0, supervisor read, PC lockmgr_exclusive+0x5d, guest dead in DDB at cycle ~12 of the kldunload-under-stream race. Instrumented markers proved fp[RX] NULLed with the xfer still live. fix.diff (unsetup+drain first) validated: 150/150 identical cycles clean.
No comments yet.