USB_FS_OPEN double-entry: two racing opens both pass the unlocked slot check and both run usbd_transfer_setup() β the second NULLs/overwrites the first's pointer, orphaning its usb_xfer (permanent M_USB leak + endpoint refcount_alloc exhaustion β endpoint dead for ALL consumers)
Summary
The EBUSY check is unlocked, setup sleeps ~200ms and NULLs its slot at entry storing only at exit: two barrier-synced opens orphan the first xfer (refcount_alloc never decremented, root never freed). Guest-reproduced AS UNPRIV maxx: 61 orphans/20s (~1MB M_USB), counters frozen at USB_EP_REF_MAX, post-cleanup USB_FS_OPEN fails ENOMEM FOREVER (endpoint dead until replug) - distinct from DF-5287 (read/write path on f->xfer[]); this is the ioctl path on f->fs_xfer[]. Fixed kernel: 600 serialized successes, zero orphans. Sentinel-claim-under-lock fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5315 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5315.c | β | 5.2 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 92 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| run.patched.log | β | 755 B | view raw | |
| env.txt | β | 664 B | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| README.md | β | 877 B | β raw | |
| manifest.json | β | 916 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-5315 β USB_FS_OPEN Γ USB_FS_OPEN double-entry: orphaned xfer + endpoint lockout
Two threads race USB_FS_OPEN on the same endpoint slot of a shared
ugen fd. Both pass the unlocked f->fs_xfer[ep] != NULL check, both
run usbd_transfer_setup(); the second NULLs/overwrites the first's
pointer β the first xfer is orphaned: leaked M_USB block + leaked
endpoint->refcount_alloc. After 63 orphans the endpoint is dead for
all consumers; memory leaks unboundedly.
Build / Run (guest, as maxx with rw on /dev/usb/0.2.0)
./build.sh ./run.sh # 20 s race + cleanup + post-cleanup probe
Success criterion (unpatched): POST-CLEANUP USB_FS_OPEN FAILED:
Cannot allocate memory + vmstat -m USB growth (~1 MB / 20 s).
On the fix-validated kernel: POST-CLEANUP OPEN SUCCEEDED.
Fix: the setup-claim hunk of ../DF-5314/fix.diff (validated there).
VERDICT β DF-5315
Status: REPRODUCED (unprivileged local user, fresh boot) β
reproduced, impact dos: permanent kernel memory leak (~1 MB per
20 s of racing; unbounded if repeated on other endpoints/devices) +
permanent endpoint lockout for every consumer (ugen ioctls,
read()/write(), kernel drivers) until replug/reboot.
Root cause
ugen_ioctl() case USB_FS_OPEN checks
f->fs_xfer[ep_index] != NULL β EBUSY with no lock
(sys/bus/u4b/usb_generic.c:1462) and then calls
usbd_transfer_setup(), which (a) sleeps for a long time
(usbd_enum_lock usb_transfer.c:942, kmalloc M_WAITOK :1220, DMA
setup) and (b) NULLs its target slot at entry β ppxfer[n] = NULL
(usb_transfer.c:935, no lock) β storing the finished pointer only at
the very end (:1138). Two barrier-synchronised opens that both pass
the EBUSY check therefore both run a full setup for the same slot; the
second entry's NULL/overwrite orphans the first usb_xfer:
- its
endpoint->refcount_alloc(incremented at usb_transfer.c:1121) is never decremented (unsetup does that at :1421) β afterUSB_EP_REF_MAX= 0x3F orphans the endpoint rejects every furtherusbd_transfer_setup()(:1118-1119); - its root block (root + xfer + DMA pages, allocated at :1220) is never freed β unbounded M_USB growth.
This is the ioctl-side sibling of DF-5287 (which races read()/write()
through ugen_transfer_setup() on f->xfer[]): different entry, different
array, same orphan mechanism β and the DF-5287/DF-5288 fixes do not
touch USB_FS_OPEN, so it survives them (verified: this race was
reproduced on a kernel that already carries the DF-5287+DF-5288 fix
semantics in the tree analysis; the guest baseline is stock #0).
Reproduction
Guest: DragonFly 6.5-DEVELOPMENT #0, QEMU x86_64, usb-ehci + usb-mouse,
unprivileged maxx on /dev/usb/0.2.0 (0666).
./df5315 20 β two threads, three barriers per round: both fire
USB_FS_OPEN simultaneously; a single serialised closer frees the
survivor. Result (run.log):
- ~60 rounds in ~6 s, essentially every round double-won (121 successful opens, 61 orphans);
- counters freeze at 60+61 (refcount exhausted);
vmstat -mUSB: 16 allocs/19.8K β 331 allocs/1.02M (+61 orphaned blocks, ~1 MB leaked in 20 s, never freeable);- after full cleanup (
USB_FS_CLOSEΓ4 +USB_FS_UNINIT+ re-INIT) a freshUSB_FS_OPENfails ENOMEM forever.
Fix validation
The fix is the USB_FS_OPEN setup-claim hunk of DF-5314/fix.diff
(self-contained there): the slot is claimed under priv_lock with a
sentinel before entering usbd_transfer_setup(), the setup runs into a
local pointer, and only the claim winner publishes. On the patched
kernel (run.patched.log): opens serialise (600 successes in 12 s), no
exhaustion, post-cleanup open succeeds, and vmstat -m shows zero
orphan growth after the whole suite.
Kernel references
- sys/bus/u4b/usb_generic.c:1456-1465 (unlocked EBUSY check)
- sys/bus/u4b/usb_generic.c:1538-1540 (setup on the shared slot)
- sys/bus/u4b/usb_transfer.c:935 (entry slot NULL, no lock)
- sys/bus/u4b/usb_transfer.c:942, :1220 (sleeping setup window)
- sys/bus/u4b/usb_transfer.c:1118-1121 (endpoint refcount)
- sys/bus/u4b/usb_transfer.c:1138 (late pointer store)
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
fixedDF-5314/fix.diff (which carries the USB_FS_OPEN setup-claim) built and booted: opens serialise (600 successes/12 s), no refcount exhaustion, POST-CLEANUP OPEN SUCCEEDED, vmstat -m USB shows zero orphan growth after the full validation suite (baseline: 61 orphans + permanent ENOMEM).
run.patched.log, ../DF-5314/fix_build.log, ../DF-5314/run.patched.log
Confirmed kernel references
Detail
Exploit chain
unpriv maxx -> shared O_RDWR fd on /dev/usb/0.2.0 -> FS_INIT -> two threads barrier-fire USB_FS_OPEN(ep0) -> both pass the unlocked slot check -> both run sleeping setups -> second entry NULLs/overwrites first's slot pointer -> orphaned xfer keeps endpoint->refcount_alloc and its ~17 KB root block -> repeat 61x -> endpoint permanently rejects all setups (ENOMEM) + unbounded kernel memory leak per endpoint/device replugged. No memory-corruption face: the orphan is never freed nor reused, only leaked.
Evidence (decisive lines)
run.log (baseline: counters freeze 60+61, POST-CLEANUP FAILED ENOMEM, vmstat 16/19.8K -> 331/1.02M), run.patched.log (600 serialised opens, POST-CLEANUP SUCCEEDED, zero orphan growth)
PoC changes
Seed sketched from the DF-5287 rig (same device, barrier-synced double-entry, refcount-exhaustion probe). Fixed two harness bugs found during verification: (1) probe-open left the slot occupied -> next round armed EBUSY (now closes the probe); (2) shutdown barrier had main participating in a racer-only 2-barrier -> deadlock (main now signals stop via flag only).
Verified recommended fix
Claim the fs_xfer slot under priv_lock with a sentinel before entering usbd_transfer_setup(), run the setup into a local pointer, publish/clear under the lock (DF-5314/fix.diff USB_FS_OPEN hunk)
Verdict
Unprivileged USB_FS_OPEN x USB_FS_OPEN on one ugen fd slot double-enters usbd_transfer_setup() (unlocked EBUSY check at usb_generic.c:1462; setup NULLs its slot at entry, usb_transfer.c:935, sleeps on enum lock + kmalloc, stores the pointer only at :1138). Every barrier-synchronised round double-won: 121 opens -> 61 orphaned usb_xfer root blocks (~1 MB M_USB leaked in 20 s, permanent) and endpoint->refcount_alloc pinned at USB_EP_REF_MAX=0x3F, so after full cleanup (CLOSE+UNINIT+re-INIT) USB_FS_OPEN fails ENOMEM forever -- the control endpoint is dead for ugen ioctls, read()/write() and kernel drivers until replug/reboot. Distinct from DF-5287 (read()/write() path on f->xfer[]): this is the ioctl path on f->fs_xfer[], and the DF-5287/5288 fixes do not cover it.
No comments yet.