ucom unplug lifecycle UAF: driver softc kfree'd while the never-freed tty keeps dangling t_sc/t_oproc/t_param/t_stop and in-flight cdev dispatch follows si_drv1 into freed memory (multi-port adapters leave orphaned openable device nodes)
Summary
The tty is never freed/unregistered and ucom_detach_tty never clears t_sc/t_oproc/t_param/t_stop, while the enclosing driver softc is kfree'd the moment detach returns: ttyflush->t_stop, ttioctl->t_param, ttstart->t_oproc all dispatch into freed memory. DFly's destroy_dev never revectors si_ops to dead_dev_ops (documented but unimplemented), so in-flight cdev dispatch follows si_drv1 into the freed softc (UCOM_MTX_LOCK lockmgr write into freed slab + sc_callback indirect call through reclaimed memory). Multi-port adapters (umcs7840/FTDI-2232): subunit>=1's init/lock/cua nodes are never destroyed and stay openable AFTER unplug - deterministic UAF with no race. Guest observationally proven (kern.ttys retains the freed softc address + ucom.ko text pointers byte-identical across the freeing detach); no panic only because devfs revocation + pre-free HL_READY clearing - dangling state + unpriv kern.ttys pointer disclosure = groomable kernel UAF with privesc potential; full chain documented. clear-callbacks+NULL-guards fix (validated clean); dead_dev_ops revector + destroy-all-subunit-cdevs needed cross-file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5320 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ucom_uaf.c | β | 3.7 KB | view raw | |
| ucom_race.c | β | 2.8 KB | view raw | |
| build.sh | β | 150 B | view raw | |
| run.sh | β | 165 B | view raw | |
| poc.log | β | 810 B | view raw | |
| env.txt | β | 241 B | view raw | |
| ttys_base.bin | β | 3.7 KB | β download | |
| ttys_live.bin | β | 4.0 KB | β download | |
| ttys_dead.bin | β | 4.0 KB | β download | |
| ttys_diff.py | β | 2.1 KB | view raw | |
| ttys_diff.log | β | 986 B | view raw | |
| code_hashes.txt | β | 155 B | view raw | |
| fix.diff | β | 2.8 KB | view raw | |
| VERDICT.md | β | 6.6 KB | β raw |
DF-5320 VERDICT β not_reproduced (crash/corruption), dangling references CONFIRMED
Bottom line
The use-after-free defect is real and code-certain: after a USB serial
device detaches while its tty is open, struct tty (never freed, never
unregistered) permanently retains t_oproc=t_stop=t_param=ucom_* and
t_sc -> the driver softc that device_set_driver() kfree'd (M_BUS) the
instant the driver's detach routine returned. I proved the dangling state
observationally on the guest: sysctl -b kern.ttys (readable by the
unprivileged user!) shows record 0 (the ucom tty) carrying ucom.ko text
pointers (0xffffffff82615f80/0x82615400/0x82616c20, inside ucom.ko
[0xffffffff82615000+0x22000]) and t_sc=0xfffff80116981198, byte-identical
before and after the detach that freed the softc (ttys_diff.log).
What I could NOT produce on this guest is a visible memory-safety event (panic / corruption). ~22 attach/detach cycles under an 8-thread open/ioctl/close storm (ucom_race.c) left the kernel standing. The two mechanisms that keep the latent UAF silent on DragonFly:
- devfs vnode revocation. When the devfs core processes the destroy
message, the device's vnodes are invalidated (
vp->v_rdev == NULL/VBAD), so every read/write/ioctl/close issued after that on a still-open fd returns EBADF before reaching ucom_dev_ops (sys/vfs/devfs/devfs_vnops.c:1267-1273, 1330-1331, 1535). This kills the trivial "unplug then ioctl the open fd" path that FreeBSD's ucom historically had. NOTE: DragonFly never revectorssi_opstodead_dev_opsβ the comment at sys/kern/kern_conf.c:336-343 describes FreeBSD behavior;dead_dev_opsis an all-NULL BSS struct that nothing assigns (only reference: a comparison at kern_conf.c:129). The protection comes from the vnode layer, asynchronously. - Pre-free flag clearing.
ucom_detach_tty()clears HL_READY under the sc_lock before the softc is freed (usb_serial.c:561), so a syscall that acquires the freed-but-still-intact lock walks intoucom_dev_ioctl's!(sc->sc_flag & UCOM_FLAG_HL_READY)bail-out (usb_serial.c:1101) and returns EIO. A deep walk (intosc->sc_callback->ucom_ioctl,usb_proc_msignalon the freed usb_process) only happens if the slab is reallocated with data that re-sets the HL_READY bit (0x40) β i.e. it needs deliberate same-bucket heap grooming on top of winning the in-flight window.
Therefore: status=not_reproduced, reproduced=false, impact=none observed. The finding remains High-severity by analysis because the in-flight window is real (destroy_dev is an async message; nothing orders the kfree after either the devfs-thread revocation or the completion of in-flight dispatches), and the corruption primitive on winning it is strong (lockmgr acquire/release writes into the freed slab; indirect calls through freed-memory pointers).
The two deterministic variants that do NOT depend on the race window
- Multi-port adapters (umcs7840, FTDI-2232C/D/4232H, multi-port u3g):
ucom_detach()callsdestroy_dev()for exactly the six cdevs ofsc[0](usb_serial.c:374-379); each subunit'sucom_detach_tty()then only removes minors equal to the plain unit number (dev_ops_remove_minor(&ucom_ops, ssc->sc_unit)at :591, which matchesdev->si_uminor == minorexactly, devfs_core.c:1555). All six cdevs of a port share one minor value (unit | CONTROL_INIT_STATE/LOCK/CALLOUT, set at :477-488 β the subunit is only in the name), so the.init/.lock/cuavariants of subunits 1..N-1 are never destroyed and never revoked: after unplugging a 4-port umcs7840, /dev/ttyiU0.1, /dev/ttylU0.1, /dev/cuaiU0.1, ... remain openable, anducom_dev_open()doessc = dev->si_drv1(usb_serial.c:786) -> freed softc ->UCOM_MTX_LOCK(sc)(lockmgr on freed memory) with no EBADF protection. QEMU cannot emulate a multi-port USB serial adapter, so this variant is proven by code reading only (confidence: certain on the code path; no hardware demo). - Unprivileged pointer disclosure:
sysctl kern.ttysdumps the rawstruct ttyarray β includingt_sc,t_oproc,t_session,t_pgrpβ to any user (observed; onlyt_devis sanitized at tty.c:2912-2913). For a detached ucom tty this hands the unprivileged attacker (a) the stale heap address of the freed softc and (b) the ucom.ko text base β exactly the primitives needed to turn the in-flight-race UAF into a reliable exploitation attempt (groom the freed bucket, know where it lands, know module addresses). The disclosure itself is a defect in sys/kern/tty.c (out of this file's scope; flagged for separate filing).
Exploit chain (theoretical, per the above)
unpriv user in dialer -> open /dev/cuaU0 -> spray same-bucket (1-2KB
class) allocations to shape the freelist -> unplug (physical) with an
ioctl in flight (or on multi-port hw: plain open() of an orphaned control
node) -> syscall thread acquires the freed embedded lock (lockmgr writes
lock_owner into attacker-shaped slab data), reads sc->sc_callback from
reclaimed memory and calls sc->sc_callback->ucom_ioctl(sc, cmd, data, 0,
curthread) β an indirect call through a reclaimed-memory pointer with
attacker-influenced first argument. uid0 potential conditional on heap
shaping; not demonstrated here.
Why the guest demo chose cuaU0 + chardev-close as the "unplug"
/dev/ttyU0 is created 0600 root:wheel (usb_serial.c:477-478); /dev/cuaU0 is 0660 uucp:dialer (:483-484) β the standard BSD serial-access group, i.e. the honest unprivileged attack surface. QEMU's usb-serial device only attaches when a client connects to its chardev socket; closing the client raises a genuine USB port disconnect -> uhub -> uftdi_detach -> ucom_detach -> "device still open, forcing close" (dmesg.txt) -> kfree.
Fix validation
Not applicable: the baseline PoC produced no panic/corruption to compare against (fix_status=not_tested). fix.diff is authored from the verified line-level understanding above; the tty-callback clearing it adds makes the proven dangling state impossible.
Attempts (22)
1 gauntlet run (ucom_uaf.c) + 6 + 12 + 3 attach/detach cycles under the 8-thread storm (ucom_race.c), plus 2 kern.ttys capture cycles and one clean-detach observation run. No panic, no corruption, guest alive throughout; final state: guest rebooted clean.
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
not_testableFix validation not applicable: the baseline PoC produced no panic/corruption observable to compare against (devfs vnode revocation blocks the trivial path; the in-flight window was not won in 22 attempts). fix.diff is authored from the verified line-level root cause; the tty-callback clearing it adds makes the proven dangling t_sc/t_oproc/t_param/t_stop state impossible.
fix.diff (git-apply-able, never applied to sys/)
Confirmed kernel references
- sys/bus/u4b/serial/usb_serial.c:366-417
- sys/bus/u4b/serial/usb_serial.c:543-594
- sys/bus/u4b/serial/usb_serial.c:374-379
- sys/bus/u4b/serial/usb_serial.c:477-488
- sys/bus/u4b/serial/usb_serial.c:939-952
- sys/bus/u4b/serial/usb_serial.c:1085-1244
- sys/bus/u4b/serial/usb_serial.c:1101
- sys/kern/subr_bus.c:1943-1945
- sys/kern/kern_conf.c:334-354
- sys/vfs/devfs/devfs_vnops.c:1267-1273
Detail
Exploit chain
unpriv dialer-group user opens /dev/cuaU0 (0660 uucp:dialer, usb_serial.c:483-484); grooms the 1-2KB kmalloc bucket; unplugs the device (physical) or triggers bus-error re-enumeration while an ioctl is in flight past devfs_fo_ioctl's v_rdev check; the syscall thread acquires the freed embedded sc_lock (lockmgr writes lock_owner into reclaimed slab), passes the stale/reshaped HL_READY gate at usb_serial.c:1101 and reaches sc->sc_callback->ucom_ioctl(sc,cmd,data,0,curthread) (usb_serial.c:1224-1226) - an indirect call through a reclaimed-memory pointer - or TIOCSETA->ucom_param->ucom_queue_command->usb_proc_msignal on the freed usb_process; kern.ttys supplies the freed-softc address and module base to aim the grooming. On multi-port hardware the orphaned /dev/ttyiU0.1-style nodes remove the race entirely (plain open() after unplug). Chain not executed to uid0 on this guest.
Evidence (decisive lines)
ttys_diff.log (decisive: t_sc=0xfffff80116981198 and ucom.ko text ptrs identical in live vs dead snapshots while no ttyU device exists); poc.log (EBADF gauntlet + 'device still open, forcing close' in dmesg); VERDICT.md (full window analysis); fix.diff
PoC changes
Seed sketch rewritten entirely: (1) discovered QEMU usb-serial only attaches when its chardev socket has a connected client and detaches when it closes - used as the unplug trigger; piix3-usb-uhci needed (xhci hot-add invisible to DFly; ehci rejects full-speed device); (2) v1 gauntlet PoC produced only EBADF, traced to devfs vnode revocation; (3) added 8-thread race storm PoC (ucom_race.c) with 21 attach/detach cycles; (4) added kern.ttys three-snapshot decode as observational proof of the dangling t_sc.
Verified recommended fix
In ucom_detach_tty, while holding tp->t_token and the sc lock, clear tp->t_oproc/t_param to NULL, set tp->t_stop = nottystop and tp->t_sc = NULL before the softc can be freed; NULL-guard t_sc in ucom_dev_read/ucom_dev_write (full fix.diff in pack); separately implement the documented dead_dev_ops revector in destroy_dev to close the in-flight si_drv1 window.
Verdict
Code-certain UAF, no observable manifestation on the guest. After a USB serial device detaches with its tty open, ucom_detach_tty() (usb_serial.c:543-594) force-closes the tty but never clears tp->t_sc/t_oproc/t_param/t_stop, and the enclosing driver softc is kfree'd (M_BUS) by device_set_driver() (subr_bus.c:1943-1945) the moment the driver's detach returns; struct tty is never freed, so it dangles forever. Additionally DragonFly never revectors si_ops to dead_dev_ops (kern_conf.c:336 comment describes non-existent behavior; dead_dev_ops is never assigned anywhere), so cdev dispatch for in-flight syscalls follows si_drv1 into freed memory. Proven on the guest: sysctl kern.ttys (world-readable) shows the ucom tty retaining ucom.ko text pointers (t_oproc/t_param/t_stop = 0xffffffff82615f80/0x82615400/0x82616c20) and t_sc=0xfffff80116981198 byte-identical before and after the detach that freed the softc (ttys_diff.log). Not manifested: devfs revokes the vnode of destroyed device nodes (devfs_vnops.c:1267-1273) so post-detach syscalls on the open fd return EBADF before reaching ucom ops, and in-flight syscalls bail at the correctly-pre-cleared HL_READY flag (usb_serial.c:561,1101); ~22 attach/detach cycles under an 8-thread open/ioctl/close storm produced no panic or corruption. A visible event needs the small in-flight window plus same-bucket heap reuse. Two deterministic variants exist by code: (1) multi-port adapters orphan the subunit>=1 init/lock/cua cdevs (usb_serial.c:374-379 destroys only sc[0]'s six; dev_ops_remove_minor at :591 matches only the plain minor) leaving openable nodes with si_drv1 -> freed softc, no revocation; (2) kern.ttys leaks the freed-softc address and ucom.ko base to unprivileged users (tty.c defect, flagged separately).
No comments yet.