# 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:

1. **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 revectors `si_ops` to
   `dead_dev_ops` — the comment at sys/kern/kern_conf.c:336-343 describes
   FreeBSD behavior; `dead_dev_ops` is 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.
2. **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 into
   `ucom_dev_ioctl`'s `!(sc->sc_flag & UCOM_FLAG_HL_READY)` bail-out
   (usb_serial.c:1101) and returns EIO.  A *deep* walk (into
   `sc->sc_callback->ucom_ioctl`, `usb_proc_msignal` on 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

1. **Multi-port adapters (umcs7840, FTDI-2232C/D/4232H, multi-port u3g):**
   `ucom_detach()` calls `destroy_dev()` for exactly the six cdevs of
   `sc[0]` (usb_serial.c:374-379); each subunit's `ucom_detach_tty()` then
   only removes minors equal to the plain unit number
   (`dev_ops_remove_minor(&ucom_ops, ssc->sc_unit)` at :591, which matches
   `dev->si_uminor == minor` exactly, 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/cua` variants 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, and
   `ucom_dev_open()` does `sc = 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).
2. **Unprivileged pointer disclosure:** `sysctl kern.ttys` dumps the raw
   `struct tty` array — including `t_sc`, `t_oproc`, `t_session`, `t_pgrp`
   — to any user (observed; only `t_dev` is 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.
