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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5314 Β· 17 files
FileTypeDescriptionSize
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.md
↓ download 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:

  1. 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) and info->setup_refcount-- twice (:1431). On this kernel USB_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.

  1. CLOSE Γ— START/COMPLETE stale-xfer use (code-proven, shallow on this kernel). ugen_fs_copy_in()/ugen_fs_copy_out() snapshot xfer = 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 sleeping copyin()s (:1069-1199: xfer->nframes, xfer->timeout, flag writes, usbd_xfer_set_frame_offset() pointer writes, and for control transfers copyin(uaddr, req, 8) writing through req = xfer->frbuffers[0].buffer read from the freed block). A concurrent USB_FS_CLOSE logically 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.

  1. USB_FS_INIT (ep_index_max=4) + USB_FS_OPEN (ep 0, control).
  2. 4 closer threads spin on a gate; the main thread releases it so all fire USB_FS_CLOSE within ~100 ns of each other, then re-arms with USB_FS_OPEN.
  3. 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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5314

Confirmed kernel references

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.