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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5368 Β· 15 files
FileTypeDescriptionSize
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:371 usb_fifo_put_bytes_max(sc->sc_fifo.fp[USB_FIFO_RX]) β€” f == NULL, free_q is at offset 0 of struct usb_fifo (usb_dev.h:103) β†’ read of VA 0x0.
  • ums.c:361 β†’ ums.c:922 usb_fifo_put_data_linear(NULL, ...) β†’ USB_IF_DEQUEUE(&f->free_q) β†’ same NULL read.
  • ums.c:363 β†’ ums_evdev_push() on a freed/zeroed sc->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).

VERDICT.md
↓ download raw

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.

  1. QEMU usb-mouse attached to piix3-usb-uhci; ums0 attaches ("5 buttons and [XYZ] coordinates ID=0").
  2. Root process (holdopen) holds ums0's evdev node (/dev/input/eventN, found via kern.evdev.input.N.name) open. Open runs evdev_register_client β†’ ums_ev_open (ums.c:964-971) β†’ ums_start_rx β€” the intr xfer runs with sc->sc_fifo never opened.
  3. Host-side monitor spam: mouse_move every 2 ms β†’ ~90 reports/s complete; each completion runs ums_intr_callback (verified earlier by reading a stream of REL events off the node).
  4. kldunload ums (software driver detach; the device stays attached and streaming β€” this is the only in-guest detach that keeps the report source alive; note usbconfig set_config on this stack never detaches the driver, and QEMU device_del kills deliveries ~250 ms before the driver detach runs, so neither works as a trigger).
  5. ~12 cycles in: Fatal trap 12, fault VA 0x0, PC lockmgr_exclusive+0x5d (resolved against /boot/kernel/kernel symbols: 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)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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)
↓ fix.diffper-fix-DF-5368

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.