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

Concurrent read()/write() on a shared ugen endpoint fd double-enters ugen_transfer_setup(): orphaned usb_xfer blocks (permanent kernel memory leak + endpoint refcount exhaustion locking the endpoint for all drivers) plus unlocked usb_fifo_free_buffer() UAF window against live callbacks

Summary

ugen_transfer_setup drops priv_lock around the sleeping usbd_transfer_setup - two threads on one fd both observe xfer[0]==NULL and both run a full setup; the second entry loop NULLs the array (usb_transfer.c:935) orphaning the first allocation whose refcounts are never dropped (1121/1132 vs 1421/1431): block never freed, and after USB_EP_REF_MAX orphans the endpoint fails setup FOREVER (silently locked for every consumer until re-plug). The second setup's usb_fifo_free_buffer (no priv_lock) kfrees fifo mbuf storage while the first setup's live callbacks iterate the queues - heap UAF window. Guest-reproduced AS UNPRIV USER: EIO marker after 2938 iterations, M_USB 16->331 allocs (19.8K->1.10M bytes), permanent endpoint lockout; write-side variant 1.24M. Busy-flag + lock-the-swap fix, validated on rebuilt kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5287 Β· 15 files
FileTypeDescriptionSize
df5287.c β€” 3.3 KB view raw
df5287b.c β€” 1.8 KB view raw
df5287c.c β€” 1.9 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 145 B view raw
run.log β€” 918 B view raw
run.3.log β€” 98 B view raw
mem_before.txt β€” 455 B view raw
mem_after.txt β€” 707 B view raw
env.txt β€” 779 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
fix.diff β€” 1.9 KB view raw
fix_validation.md β€” 1.4 KB ↓ raw
fix_build.log β€” 936 B view raw
run.patched.log β€” 345 B view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-5287

Status: REPRODUCED (unprivileged local user, fresh-boot guest, no kernel modifications) β€” reproduced, impact dos (kernel memory leak + permanent endpoint lockout; same race also opens an unlocked usb_fifo_free_buffer() UAF window, documented below).

How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT (with-src snapshot), QEMU x86_64, -device usb-ehci + -device usb-mouse (ugen0.2, interrupt-IN endpoint node /dev/usb/0.2.1). Unprivileged user maxx (uid 1001, member of operator, nodes chmod 0660 β€” the standard BSD "libusb user" configuration; default DragonFly ships these nodes 0600 root:operator).

  1. Baseline: vmstat -m β†’ USB 16 allocs / 19.8K.
  2. ./df5287 3000 β€” two threads race read() on one shared O_RDONLY|O_NONBLOCK fd, close+reopen per iteration.
  3. After 2938 iterations the PoC printed its marker: EIO after 2938 iterations (endpoint refcount_alloc exhausted by orphaned usb_xfers -- double-setup race won >= 61 times).
  4. vmstat -m β†’ USB 331 allocs / 1.10M (2.95K requests): ~1 MB of never-freeable kernel memory accumulated from the orphaned xfer blocks.
  5. Re-running the PoC immediately EIOs at iteration 0 β€” the endpoint stays dead (refcount_alloc pinned at USB_EP_REF_MAX) until the device is re-plugged; usbconfig dump_info still shows a healthy device, so this is a silent lockout of the endpoint for every consumer, kernel drivers included.
  6. Write-side variant (df5287c, usb-storage bulk-OUT endpoint 0.2.2, umass not attached): same leak signature β€” M_USB grew 34.3K β†’ 1.24M within ~1000 iterations before the run was stopped (log run.3.log).

Why the leak is permanent

Each race-won iteration leaves one usbd_transfer_setup() allocation unreferenced:

  • usbd_transfer_setup() incremented xfer->endpoint->refcount_alloc (sys/bus/u4b/usb_transfer.c:1121) and info->setup_refcount (usb_transfer.c:1132);
  • the second setup's entry loop wrote ppxfer[n] = NULL over the first setup's pointers (usb_transfer.c:935 β€” ppxfer[n] = NULL; in the initial sanity loop), so usbd_transfer_unsetup() never sees them again;
  • therefore neither refcount_alloc-- (usb_transfer.c:1421) nor info->setup_refcount-- (usb_transfer.c:1431) ever runs for the orphan, and the memory block is never freed.

The UAF window (same root cause, harder to time)

ugen_transfer_setup() calls usb_fifo_alloc_buffer() -> usb_fifo_free_buffer() (sys/bus/u4b/usb_dev.c:2009-2020) after re-acquiring... actually before re-acquiring priv_lock (usb_generic.c:145-166 in the unfixed tree: the buffer swap sits between lockmgr(f->priv_lock, LK_RELEASE) and the final lockmgr(f->priv_lock, LK_EXCLUSIVE)). usb_fifo_free_buffer() does kfree(f->queue_data) + memset of both queues with no lock at all, while the winning setup's live callbacks (ugen_ctrl_read_callback -> usb_fifo_put_data(), usb_dev.c:2064; ugen_ctrl_write_callback -> usb_fifo_get_data(), usb_dev.c:2176) iterate those queues under priv_lock. A racing second setup therefore frees the mbuf storage under a running callback: writes into freed M_USBDEV heap + corrupted queue linkage feeding read()/write(). This window was not converted into a demonstrated panic within the time budget (it needs a callback in flight inside a ~Β΅s-wide free window; the mouse endpoint provides no traffic in a headless QEMU and the storage write-path was used for the leak runs), but it follows directly from the same unlocked code path. The fix removes it.

Fix validation

See fix_validation.md / fix_build.log / run.patched.log β€” with fix.diff (busy-flag in ugen_transfer_setup() + fifo buffer swap under priv_lock) applied to a rebuilt kernel, the exact same PoC ran to completion without the EIO marker and without M_USB growth.

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_validation.md
↓ download raw

Fix validation β€” DF-5287 (combined kernel with DF-5288 fix)

Baseline (unpatched with-src kernel)

check result
./df5287 3000 (read side) EIO after 2938 iterations — endpoint refcount exhausted; vmstat -m USB 16→331 allocs / 19.8K→1.10M; permanent EIO on re-run at iteration 0
./df5287c (write side, usb-storage bulk-OUT) M_USB 34.3K β†’ 1.24M before the run was stopped (run.3.log, mem_after.txt)

Patched kernel (fix.diff: flag_setup_busy guard in

ugen_transfer_setup() + fifo buffer swap under priv_lock)

Kernel: same validated build as DF-5288 (#1: Fri Oct 2 05:05:50 UTC 2026, BUILD_RC=0/INSTALL_RC=0).

check result
./df5287 3000 no EIO after 3000 iterations, rc=0 (run.patched.log) β€” endpoint never locks out
vmstat -m across the run USB allocation count flat: 270 → 270 (32.0K total requests; ~3000 additional open/setup/close cycles added zero leaked allocations). The small MemUse delta (844K→908K) with a flat count is transient churn of the 256-slot delayed-free ring, not permanent orphans — compare the unpatched run where the count grew without bound and the endpoint died.
read()/write() functional PoC threads performed 4 non-blocking reads per open Γ— 3000 opens with no spurious EIO/EBUSY beyond the race marker; guest stayed up throughout

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

With fix.diff applied to a rebuilt -Werror kernel (BUILD_RC=0, INSTALL_RC=0, kern.version #1 Fri Oct 2 05:05:50 UTC 2026), the identical PoC completes 3000 iterations with NO EIO marker and a flat USB allocation count (270->270 over ~3000 open/setup/close cycles), versus permanent endpoint lockout and unbounded allocation growth on the unpatched baseline.

fix_validation.md, run.patched.log, fix_build.log
↓ fix.diffper-fix-DF-5287

Confirmed kernel references

Detail

Exploit chain

unpriv user opens ugen endpoint node -> two threads read()/write() the same fd -> both pass the f->xfer[0]==NULL check while priv_lock is dropped -> duplicate usbd_transfer_setup -> ppxfer[n]=NULL clobbers loser's pointers -> permanent kmalloc leak + endpoint refcount exhaustion -> endpoint dead for all drivers (DoS) / racing usb_fifo_free_buffer() kfree()s mbuf storage under a live usb_fifo_put_data() callback (heap UAF window)

Evidence (decisive lines)

run.log (EIO marker + mem numbers), mem_before.txt/mem_after.txt, run.3.log (write side), VERDICT.md, fix_validation.md (patched kernel: 'no EIO after 3000 iterations', flat allocation count), fix.diff (git-apply clean)

PoC changes

PoC written fresh for this finding (no seed). df5287.c uses O_NONBLOCK reads on a shared fd with close/reopen cycles to re-arm the vulnerable window; df5287c.c is the write-side variant against a usb-storage bulk-OUT endpoint; df5287b.c is the blocking/traffic variant kept for UAF hunting.

Verified recommended fix

Serialize ugen_transfer_setup() with a per-fifo busy flag and perform the usb_fifo_alloc_buffer()/free_buffer() swap under priv_lock (see fix.diff).

Verdict

REPRODUCED on the QEMU guest as unprivileged user maxx (uid 1001, operator group, 0660 ugen nodes): two threads sharing one ugen endpoint fd race ugen_transfer_setup()'s unlocked window (priv_lock dropped around the sleeping usbd_transfer_setup), double-entering the setup. The second setup's entry loop NULLs the first's f->xfer[] pointers (usb_transfer.c:935), orphaning live usb_xfer blocks: endpoint refcount_alloc and info->setup_refcount are never decremented, so the memory is never freed and after USB_EP_REF_MAX(0x3f) orphans usbd_transfer_setup() fails forever, silently locking the endpoint for every driver until re-plug. Demonstrated: read-side marker 'EIO after 2938 iterations' with vmstat -m USB growth 16->331 allocs / 19.8K->1.10M and permanent EIO on re-run; write-side (usb-storage bulk-OUT) 34.3K->1.24M. The same unlocked region runs usb_fifo_alloc_buffer()->usb_fifo_free_buffer() (no priv_lock) against live fifo callbacks, a heap-UAF window documented in VERDICT.md but not converted to a demonstrated panic. Fix (busy-flag + buffer swap under priv_lock) validated on a rebuilt kernel: no EIO in 3000 iterations, allocation count flat.