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)
PoC verification
Evidence pack
findings/poc/DF-5287 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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).
- Baseline:
vmstat -mβUSB 16 allocs / 19.8K. ./df5287 3000β two threads raceread()on one shared O_RDONLY|O_NONBLOCK fd, close+reopen per iteration.- 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). vmstat -mβUSB 331 allocs / 1.10M(2.95K requests): ~1 MB of never-freeable kernel memory accumulated from the orphaned xfer blocks.- 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_infostill shows a healthy device, so this is a silent lockout of the endpoint for every consumer, kernel drivers included. - 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 (logrun.3.log).
Why the leak is permanent
Each race-won iteration leaves one usbd_transfer_setup() allocation
unreferenced:
usbd_transfer_setup()incrementedxfer->endpoint->refcount_alloc(sys/bus/u4b/usb_transfer.c:1121) andinfo->setup_refcount(usb_transfer.c:1132);- the second setup's entry loop wrote
ppxfer[n] = NULLover the first setup's pointers (usb_transfer.c:935 βppxfer[n] = NULL;in the initial sanity loop), sousbd_transfer_unsetup()never sees them again; - therefore neither
refcount_alloc--(usb_transfer.c:1421) norinfo->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 β 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
fixedWith 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
Confirmed kernel references
- sys/bus/u4b/usb_generic.c:145
- sys/bus/u4b/usb_generic.c:151
- sys/bus/u4b/usb_generic.c:156
- sys/bus/u4b/usb_generic.c:228
- sys/bus/u4b/usb_generic.c:296
- sys/bus/u4b/usb_transfer.c:935
- sys/bus/u4b/usb_transfer.c:1117
- sys/bus/u4b/usb_transfer.c:1121
- sys/bus/u4b/usb_transfer.c:1421
- sys/bus/u4b/usb_transfer.c:1431
- sys/bus/u4b/usb_dev.c:1984
- sys/bus/u4b/usb_dev.c:2009
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.
No comments yet.