# DF-5191 VERDICT — sio_pccard_detach races the sio core without tty_token → UAF / double-free of com->ibuf and duplicated tty close

**File:** `sys/dev/serial/sio/sio_pccard.c` (pass 2, GLM 5.3)
**Lines:** 117–143 (`sio_pccard_detach`)

## Status: NOT REPRODUCED ON GUEST — code-trace proof only

The audit guest is QEMU/i440fx: no PCMCIA bridge, `pccard` absent from
dmesg.boot, `sio_pccard.c` is compiled only for `optional sio pccard`
(sys/platform/pc64/conf/files:197). No sio-pccard device can exist on this
guest, so the race has no runtime surface here. The defect is established by
inspection below; reproduction requires real pccard hardware (see
`race_harness.c` for the exact interleaving harness).

## Root cause

Every path in the sio core that touches `com->gone`, `com->tp`, `com->ibuf`
or the UART runs serialized under `lwkt_gettoken(&tty_token)`:

* `sioopen` — token at sys/dev/serial/sio/sio.c:1277 (gone-check at :1273
  deliberately *before* the token — see below)
* `sioclose` — token at sio.c:1460
* `siosetwater` — "called with tty_token held" comment, sio.c:2446
* `sioparam`, `comstop`, `siosettimeout`, intr paths — same token

`sio_pccard_detach()` mutates exactly those fields with **no token at all**
(sio_pccard.c:117-143 — `lwkt_gettoken` does not appear in the file):

```c
com->gone = 1;                                        /* :127 */
...
if (com->tp && (com->tp->t_state & TS_ISOPEN)) {      /* :134 */
        device_printf(dev, "still open, forcing close\n");
        ttyclose(com->tp);                            /* :136 */
} else {
        if (com->ibuf != NULL)                        /* :138 */
                kfree(com->ibuf, M_DEVBUF);           /* :139 — never NULLed */
}
```

## Concrete interleavings (all fields unsynchronized)

**Pair 1 — detach ∥ sioclose → double-free of `com->ibuf`.**
Thread A is inside `sioclose` (token held) between `comhardclose` and the
`com->gone` re-check at sio.c:1475. Thread B (detach) sets `gone=1`, reads
`TS_ISOPEN` as clear (A has not reached `ttyclose` yet / t_state is being
torn down concurrently), frees `com->ibuf` at :139 and does not NULL it.
Thread A then executes `if (com->gone) { if (com->ibuf != NULL)
kfree(com->ibuf …) }` (sio.c:1475-1479) — **second kfree of the same
M_DEVBUF chunk**. Heap corruption of the kernel malloc zone (ibuf is a
1024–8192-byte M_DEVBUF block, siosetwater). This is the detach-side twin of
pass-1 DF-3902 (sioclose-side non-NULLed pointer); the missing token is what
makes the detach-side free racy in the first place.

**Pair 2 — detach ∥ sioopen → UAF use of `com->ibuf` + later double-free.**
`sioopen` checks `com->gone` at sio.c:1273 *before* taking the token, so an
open in flight when the card is ejected continues into `ttymalloc`,
`comhardopen` (which sleeps seconds waiting for DTR — `tsleep(…
"siodtr"…)` at sio.c:1284-1295, and the core even rechecks `com->gone`
afterwards because ejection-during-open was anticipated). During that sleep
detach runs: `TS_ISOPEN` is not set yet (linesw `l_open` runs only after
`comhardopen` returns), so detach takes the `else` branch and frees
`com->ibuf`. The open then resumes with `com->ibuf` dangling —
`com->iptr = com->ibuf` (sio.c:1698) and the input path `bcopy(com->ibuf…)`
(sio.c:1703-1708) operate on freed memory (UAF write). When the user closes,
`sioclose`'s gone-branch frees the same pointer again → double-free.

**Pair 3 — detach ∥ sioclose → duplicated `ttyclose`.**
Detach's forced `ttyclose(com->tp)` (:136) runs while a concurrent
`sioclose` executes `(*linesw[tp->t_line].l_close)()` + `comhardclose` +
`ttyclose` (sio.c:1468-1472) under the token. `ttyclose` itself takes only
`tp->t_token` (sys/kern/tty.c:247), which does not serialize against the
linesw close sequence: queues are flushed and `clist_free_cblocks()` runs
three times (tty.c:256-258) in both threads — the second `ttyclose` on the
already-torn-down tty re-frees clist blocks and scrambles `t_state` while
the first close is still inside `l_close`.

**Pair 4 — detach ∥ siosetwater (TIOCSETAW speed change) → ibufold UAF.**
`siosetwater` (token held, sio.c:2446+) swaps `com->ibufold = com->ibuf`,
installs a new `com->ibuf`, and frees `ibufold` at its tail (sio.c:2438-2441).
A tokenless detach interleaving frees `com->ibuf` (old or new depending on
the swap point) while `siosetwater` still holds/frees the other alias of the
same chunk — at minimum one UAF access (`com->iptr` still points into the
old buffer, sio.c:1698) and one double-free.

## Trigger / threat model

`device_detach` for a pccard sio child fires on card ejection (physical),
`pccardc power off` (root), or controller `kldunload` (root) — the file's own
comment says so (sio_pccard.c:108-115). The racing counterpart only needs
the tty device node open/closed — an unprivileged user on a multi-user
machine. Timing window is wide (DTR wait alone sleeps seconds). Impact class
is kernel heap UAF/double-free (M_DEVBUF) — corruption, in principle
exploitable; practically gated behind a card-eject event. Medium severity,
consistent with the sibling DF-2191 (same function, different lifetime bug:
callouts not drained).

## Relationship to known findings

* **DF-2191** (pass 1): callouts `dtr_ch`/`busy_ch` not stopped/drained
  before softc free — different objects, same function. My fix.diff includes
  the callout drain so the function is fixed as a whole; the finding credit
  for that part stays with DF-2191.
* **DF-3902** (pass 1, sio.c): sioclose gone-path double-free due to
  non-NULLed `com->ibuf` — the *deterministic* close-side consequence.
  DF-5191 supplies the missing synchronization that enables the *detach-side*
  racy frees (and covers pairs 2–4, which DF-3902 does not).
* sio isa/pci/puc attachments have **no** detach method at all
  (sio.c:193-238) — pccard is the only detachable sio, so this race family
  exists only through this file.

## Fix

Take `tty_token` around the whole detach body, NULL the freed pointers, and
drain the callouts (DF-2191). See `fix.diff` (authored against the read-only
sys/ tree; never applied there). Not validated on the guest: no pccard
hardware, and a kernel build cannot exercise a bus that does not exist.

## Guest

`DragonFly dfbsd 6.5-DEVELOPMENT #0 … X86_64_GENERIC`; `grep -c pccard
/var/run/dmesg.boot` = 0; no PCMCIA bridge in pciconf -l. Guest left clean
(reset with-src).

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