USB_FS_CLOSE is unserialized: concurrent closes double-run usbd_transfer_unsetup() on one xfer (kernel panic via setup_refcount assert; refcount_alloc underflow β permanent endpoint lockout on no-assert kernels) and CLOSEΓSTART leaves stale-xfer UAF writes into freed memory
Summary
USB_FS_CLOSE calls unsetup with no lock while unsetup reads its slot pointer BEFORE taking the xfer lock - two closers both read non-NULL and both run full unsetup: refcounts decremented twice, second hits USB_ASSERT 'Invalid setup reference count' -> panic. Also START/COMPLETE snapshot fs_xfer[ep] unlocked and write user bytes into the xfer across sleeping copyins while CLOSE frees it (UAF write; non-crashing on shipped kernel only due to per-type single-size zones + delayed-free ring - documented honestly). Guest-reproduced AS UNPRIV maxx: spin-gated CLOSE x4 -> panic 3/3 fresh boots; fixed kernel SURVIVED 3000/3000 rounds + FS smoke 5/5. Slot-claim-under-priv_lock fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5314 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5314.c | β | 4.3 KB | view raw | |
| df5314b.c | β | 5.3 KB | view raw | |
| df5314c.c | β | 4.2 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 411 B | view raw | |
| run.log | β | 883 B | view raw | |
| run.2.log | β | 374 B | view raw | |
| run.patched.log | β | 407 B | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| env.txt | β | 664 B | view raw | |
| fix.diff | β | 12.8 KB | view raw | |
| usb_generic.fixed.c | β | 56.5 KB | view raw | |
| fix_build.log | β | 620 B | view raw | |
| VERDICT.md | β | 7.5 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
VERDICT β DF-5314
Status: REPRODUCED (unprivileged local user, fresh boots, no kernel
modifications) β reproduced, impact panic (kernel assertion failure
from a double usbd_transfer_unsetup() of the same xfer; on kernels
without USB_ASSERT compiled in the same race instead underflows
endpoint->refcount_alloc to 0xFF and permanently locks the endpoint
out for every consumer). The finding covers the unserialized
USB_FS_CLOSE side of the f->fs_xfer[] state machine in
ugen_ioctl() β the sibling of DF-5288 whose freeing side there is
USB_FS_UNINIT.
What the finding is
ugen_ioctl() dispatches the whole USB_FS_* family with no
serialization (the cdev ioctl path takes no fifo lock,
sys/bus/u4b/usb_dev.c:1115, and no enum lock for f_ioctl,
usb_dev.c:1094). Specifically USB_FS_CLOSE
(sys/bus/u4b/usb_generic.c:1558-1568) does:
usbd_transfer_unsetup(f->fs_xfer + u.pclose->ep_index, 1);
and usbd_transfer_unsetup() reads its array entry before taking
the xfer lock (sys/bus/u4b/usb_transfer.c:1379 xfer = pxfer[n_setup];
β¦ :1386 USB_XFER_LOCK(xfer); β¦ :1405 pxfer[n_setup] = NULL;). Two
consequences:
- CLOSE Γ CLOSE double-unsetup (reproduced, panics). Two
concurrent closes that both read the non-NULL pointer before either
NULLs it both run a full unsetup on the same xfer:
xfer->endpoint->refcount_alloc--twice (usb_transfer.c:1421, 1 β 0 β 0xFF) andinfo->setup_refcount--twice (:1431). On this kernelUSB_ASSERT(info->setup_refcount != 0, β¦)(usb_transfer.c:1428) is live, so the second decrement panics:
panic: Invalid setup reference count
usbd_transfer_unsetup() at usbd_transfer_unsetup+0x16c
usbd_transfer_unsetup() at usbd_transfer_unsetup+0x16c
ugen_ioctl() at ugen_ioctl+0x109
(panic.txt; hit 3/3 times on fresh boots β twice during
exploration, once in the documented run where the spin-gated PoC
df5314c panics in round 1, run.2.log.) On a kernel without
that assert the result is the underflow: refcount_alloc pinned at
0xFF β₯ USB_EP_REF_MAX (0x3F) β the endpoint permanently rejects
every usbd_transfer_setup() (usb_transfer.c:1118-1119) for ugen
ioctls, read()/write() and kernel drivers alike, until replug.
- CLOSE Γ START/COMPLETE stale-xfer use (code-proven, shallow on
this kernel).
ugen_fs_copy_in()/ugen_fs_copy_out()snapshotxfer = f->fs_xfer[ep_index]with no lock (usb_generic.c:1051, :1229 β the same consumer sites DF-5288 identified for UNINIT) and then write transfer parameters into the xfer across several sleepingcopyin()s (:1069-1199:xfer->nframes,xfer->timeout, flag writes,usbd_xfer_set_frame_offset()pointer writes, and for control transferscopyin(uaddr, req, 8)writing throughreq = xfer->frbuffers[0].bufferread from the freed block). A concurrentUSB_FS_CLOSElogically frees the xfer mid-loop.
Why variant A does not crash on the shipped kernel (honest
negative, run.log: 3072+ cycles survived twice): DragonFly kmalloc
zones are per-malloc-type, single-size (sys/kern/kern_kmalloc.c:42-52
β "zones don't stomp over each other"), so the freed M_USB block can
only be reclaimed by another same-shaped xfer block (benign
aliasing), never by attacker-controlled foreign data; and
usbd_transfer_unsetup_sub() releases the memory only through the
256-slot delayed-free ring (usbd_delayed_free,
usb_transfer.c:1333-1357), which keeps the content intact long past
the race window. The stale writes therefore land in
logically-freed but intact (or same-typed reallocated) memory. The
primitive is real β an unprivileged user writing attacker-chosen
nframes/frlengths/request bytes into freed kernel memory β but
on this kernel configuration it does not produce a user-visible
fault, so it is documented rather than claimed.
Variant B (df5314b.c, barrier-synchronised double-close) exercised
the same window as variant C with microsecond-scale entry skew and
did not land in 5000 rounds ([!] round 2 note: the first harness
version had a barrier-count bug; fixed version ran clean); the
spin-gate variant C compresses the entry skew to ~100 ns and lands
deterministically (round 1).
Reproduction (variant C, df5314c.c)
Guest: DragonFly 6.5-DEVELOPMENT (with-src snapshot, kernel #0),
QEMU x86_64, -device usb-ehci + -device usb-mouse; unprivileged
maxx (uid 1001, not in operator) on /dev/usb/0.2.0 chmod 0666.
USB_FS_INIT(ep_index_max=4) +USB_FS_OPEN(ep 0, control).- 4 closer threads spin on a gate; the main thread releases it so all
fire
USB_FS_CLOSEwithin ~100 ns of each other, then re-arms withUSB_FS_OPEN. - Round 1: two closers both read
pxfer[0]before either NULLs it β double unsetup βpanic: Invalid setup reference count, guest dead in DDB (panic.txt). 3/3 fresh-boot baseline runs panicked.
Fix validation (fix.diff, this pack)
fix.diff (19 hunks, self-contained) adds a slot-claim protocol:
USB_FS_CLOSE now clears the slot under priv_lock and unservices the
claimed pointer, so exactly one closer ever runs an unsetup; it also
carries the DF-5288-style priv_lock-held ugen_fs_copy_in/out()
(poison-aware), the USB_FS_OPEN setup claim (DF-5315), the
USB_FS_STOP u.pstartβu.pstop typo fix, a locked snapshot in
USB_FS_CLEAR_STALL_SYNC, and ugen_fs_uninit() locking. Built and
booted (fix_build.log, kernel #1 15:25:39 UTC):
| check | baseline | patched |
|---|---|---|
df5314c (3000 spin-gated double-close rounds) |
panic in round 1 (3/3 boots) | SURVIVED 3000/3000, guest up |
df5315 (double-open) |
endpoint dead, 1 MB leaked | serialised, no exhaustion (run.patched.log) |
df5288 (regression) |
panics β€ 60 s | survived 100 s (rc=124) |
| FS smoke (libusb-style GET_STATUS Γ5) | β | PASS 5/5 (no functional regression) |
vmstat -m USB after full suite |
331 allocs / 1.02 MB after 20 s of racing | 270 / 844 KB after a much heavier suite (no orphans) |
Exploitability notes
- Panic from an unprivileged user: trivial (variant C).
- The underflow face (no-assert kernels) is a permanent endpoint lockout (same impact class as DF-5287's exhaustion half).
- The stale-write face (variant A) is a genuine use-after-free write of semi-controlled data into freed kernel memory; on the shipped per-type-zone kernel it is not convertible to a crash with the primitives available to an unprivileged user (no cross-type heap spray can reach an M_USB zone). Should the allocator ever share zones by size class, this becomes a groomable write primitive.
Kernel references
- sys/bus/u4b/usb_generic.c:1558-1568 (USB_FS_CLOSE, no lock)
- sys/bus/u4b/usb_generic.c:1051-1060, :1229-1238 (unlocked consumer snapshots)
- sys/bus/u4b/usb_generic.c:1590 (unlocked
->endpointderef) - sys/bus/u4b/usb_transfer.c:1379-1405 (read-before-lock in unsetup)
- sys/bus/u4b/usb_transfer.c:1421, :1428-1431 (refcount decrement + assert)
- sys/bus/u4b/usb_transfer.c:1333-1357 (delayed-free ring)
- sys/kern/kern_kmalloc.c:42-52 (per-type single-size zones)
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
fixedfix.diff (19 hunks) applied to pristine /usr/src, make nativekernel BUILD_RC=0, installkernel INSTALL_RC=0, booted kernel #1 15:25:39: df5314c survived 3000/3000 spin-gated rounds (baseline panicked round 1, 3/3 boots); DF-5288 PoC regression survived; FS smoke 5/5 PASS; vmstat -m USB shows no orphan growth after the full suite.
fix_build.log, run.patched.log, fix_validation section of VERDICT.md
Confirmed kernel references
- sys/bus/u4b/usb_generic.c:1558-1568
- sys/bus/u4b/usb_generic.c:1051-1060
- sys/bus/u4b/usb_generic.c:1229-1238
- sys/bus/u4b/usb_generic.c:1590
- sys/bus/u4b/usb_transfer.c:1379-1405
- sys/bus/u4b/usb_transfer.c:1421
- sys/bus/u4b/usb_transfer.c:1428-1431
- sys/bus/u4b/usb_transfer.c:1333-1357
- sys/kern/kern_kmalloc.c:42-52
Detail
Exploit chain
unpriv maxx -> open /dev/usb/0.2.0 O_RDWR -> USB_FS_INIT + USB_FS_OPEN(ep0) -> 4 threads spin-gate-release USB_FS_CLOSE simultaneously -> two unservices both read pxfer[0] non-NULL -> double drain/deref -> second setup_refcount-- hits USB_ASSERT -> panic: Invalid setup reference count (guest dead in DDB). No uid0 chain on this kernel: the stale-write UAF face cannot be cross-type groomed (DFly kmalloc zones are per-malloc-type single-size) and freed blocks stay intact through the 256-slot delayed-free ring.
Evidence (decisive lines)
panic.txt (double usbd_transfer_unsetup stack + assert message), run.2.log (variant C: 'round 1 survived' then guest death), run.patched.log (3000/3000 survived on fix-validated kernel), run.log (variant A negative + allocator analysis), fix.diff/fix_build.log (19 hunks, BUILD_RC=0, kernel #1 15:25:39)
PoC changes
Variant A seeded from the DF-5288 rig conventions (same device/structs). Variant B added barrier sync (first version had a barrier-count bug: 3 threads on a 2-barrier -> self-deadlock, fixed). Variant C replaces the barrier with a spin gate so all closers enter ioctl within ~100ns -- lands deterministically (round 1) where the barrier version's us-scale skew missed in 5000 rounds.
Verified recommended fix
Claim the fs_xfer slot under priv_lock in USB_FS_CLOSE (NULL it, then unsetup the claimed pointer) + priv_lock-held, poison-aware copy_in/copy_out + setup-claim in USB_FS_OPEN (see fix.diff)
Verdict
Unprivileged USB_FS_CLOSE x USB_FS_CLOSE on a shared ugen fd double-runs usbd_transfer_unsetup() on the same xfer (slot pointer read at usb_transfer.c:1379 before the locked NULL at :1405): panic 'Invalid setup reference count' (USB_ASSERT at :1428) with two usbd_transfer_unsetup frames under ugen_ioctl -- reproduced 3/3 fresh boots, spin-gated PoC panics in round 1. On no-assert kernels the same race underflows endpoint->refcount_alloc to 0xFF (permanent endpoint lockout). The CLOSE x START face writes attacker-chosen nframes/frlengths/request bytes into freed-but-intact kernel memory (per-type kmalloc zones + 256-slot delayed-free ring shield it from crashing on the shipped kernel -- documented, not claimed). fix.diff validated: baseline panics gone, no functional regression.
No comments yet.