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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5315 Β· 10 files
FileTypeDescriptionSize
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.md
↓ download raw

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) β†’ after USB_EP_REF_MAX = 0x3F orphans the endpoint rejects every further usbd_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 -m USB: 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 fresh USB_FS_OPEN fails 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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

DF-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
per-fix-DF-5315

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.