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

sio_pccard_detach mutates com->gone/com->tp/com->ibuf and calls ttyclose without tty_token, racing siopen/sioclose/siosetwater into kernel heap UAF and double-free of com->ibuf

Summary

The only detachable sio path bypasses the tty_token protocol every core path holds - four concrete interleavings reach double kfree of ibuf (freed without NULLing), UAF writes through com->iptr under an open sleeping in the DTR wait, duplicated linesw-close/ttyclose sequences, and an ibufold-alias double free. Unpriv holds the tty open/close; the detach needs eject/kldunload (Medium, DF-2191 tier). Hardware-gated (no PCMCIA on guest); proven by lock-protocol trace. Token+drain+NULL fix (coordinates DF-2191/DF-3902).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5191 Β· 7 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 6.7 KB ↓ raw
race_harness.c β€” 2.0 KB view raw
fix.diff β€” 2.7 KB view raw
verdict.json β€” 5.1 KB view raw
manifest.json β€” 1.1 KB view raw
env.txt β€” 264 B view raw

DF-5191 β€” sio_pccard_detach races the sio core without tty_token (UAF / double-free)

Verdict in one line

sys/dev/serial/sio/sio_pccard.c:117-143 β€” sio_pccard_detach() sets com->gone, calls ttyclose(com->tp) and frees com->ibuf without taking tty_token, the exact token every sio-core path holds while touching those fields (sioopen sio.c:1273/1277, sioclose sio.c:1460/1475-1482, siosetwater sio.c:2446). Card eject (or pccardc power off / kldunload) racing an open/close in flight double-frees com->ibuf (M_DEVBUF) or double-executes the tty close sequence. Medium / likely / CWE-362 β†’ CWE-415/CWE-416.

Reproduce

Cannot run on the audit guest (QEMU i440fx: no PCMCIA bridge, no pccard in dmesg.boot; sio_pccard.c is optional sio pccard). On hardware, or on any DragonFly system with a pccard serial device:

cc -O2 -o race_harness race_harness.c
./race_harness /dev/ttyd0 &          # unprivileged
# as root, repeatedly:
pccardc power off 0 ; sleep 1 ; pccardc power on 0

Success criterion: kernel panic or M_DEVBUF double-free diagnostics.

Contents

  • VERDICT.md β€” four concrete interleavings (detachβˆ₯close double-free, detachβˆ₯open UAF+double-free, duplicated ttyclose/clist free, ibufold UAF), relationship to DF-2191 / DF-3902, why isa/pci/puc sio are unaffected (they have no detach method at all)
  • race_harness.c β€” illustrative trigger harness (not runnable on guest)
  • fix.diff β€” token + drain + NULL-out fix (coordinates DF-2191/DF-3902)
  • verdict.json / manifest.json
VERDICT.md
↓ download raw

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

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation impossible on the audit guest: no pccard hardware means no baseline detach event exists to race against, patched or unpatched. fix.diff is a lock-protocol-reviewed proposal (release-drain-retake ordering chosen because siodtrwakeup/siobusycheck acquire tty_token, so draining under the token would deadlock).

n/a - no runnable baseline on guest
↓ fix.diffper-fix-DF-5191

Confirmed kernel references

Detail

Exploit chain

root/physical triggers pccard detach; unprivileged user holds open/close in flight on /dev/ttydX; detach's tokenless kfree(com->ibuf) lands while sioclose/sioopen/siosetwater still reference it -> M_DEVBUF (1024-8192 byte) double free / UAF write through com->iptr -> kernel heap corruption. Chain NOT developed further: no runtime surface on the audit guest (no pccard hardware), so exploitation could not be attempted.

Evidence (decisive lines)

VERDICT.md (interleaving-by-interleaving trace with path:line); race_harness.c (trigger harness for hardware owners); fix.diff (token + callout_drain + NULL-out fix coordinating DF-2191/DF-3902); env.txt (guest shows no pccard)

PoC changes

No seed PoC existed. race_harness.c written fresh; explicitly documented as non-runnable on the audit guest.

Verified recommended fix

Serialize sio_pccard_detach against the sio core: publish com->gone under tty_token, drop the token, callout_drain dtr_ch/busy_ch (callbacks take the token themselves), re-take the token for the teardown, and NULL com->ibuf/com->ibufold after kfree - see fix.diff

Verdict

Code-trace-proven race, not runnable on the audit guest: the guest is QEMU/i440fx with no PCMCIA/pccard hardware (0 'pccard' lines in dmesg.boot; sio_pccard.c compiles only for 'optional sio pccard', sys/platform/pc64/conf/files:197), so no sio-pccard device and no detach event can exist there. The defect is established by inspection: sio_pccard_detach (sio_pccard.c:117-143) sets com->gone, calls ttyclose(com->tp) and kfrees com->ibuf without acquiring tty_token, while every sio-core path touching those fields holds it (sioopen gone-check sio.c:1273 sits BEFORE its lwkt_gettoken at sio.c:1277; sioclose takes the token at sio.c:1460 and re-frees com->ibuf at sio.c:1475-1482 when com->gone is set; siosetwater documents 'called with tty_token held' at sio.c:2446 and frees the ibuf alias com->ibufold at sio.c:2438-2441; the callout callbacks siodtrwakeup/siobusycheck also run under the token). Four interleavings reach memory corruption: (1) detach frees com->ibuf (sio_pccard.c:138-139, never NULLed) while a token-holding sioclose passes its gone-check and kfrees the same M_DEVBUF chunk - double free; (2) an open that passed the pre-token gone-check sleeps in comhardopen (DTR wait, sio.c:1284-1295) while detach takes the !TS_ISOPEN branch and frees com->ibuf underneath it - com->iptr = com->ibuf (sio.c:1698) and bcopy(com->ibuf,...) (sio.c:1703-1708) become UAF, and the later close double-frees; (3) detach's forced ttyclose (sio_pccard.c:136) duplicates the linesw-close+ttyclose sequence a concurrent sioclose runs (sio.c:1468-1472), re-freeing clist blocks; (4) detach interleaved with siosetwater's ibuf/ibufold swap frees an alias siosetwater still frees at its tail. Trigger requires a card-eject/power-off/modunload event (physical or root) racing an unprivileged open/close - wide window (seconds of DTR wait). Same function as DF-2191 (callouts) and the detach-side twin of DF-3902 (sioclose-side non-NULLed ibuf); isa/pci/puc sio attachments have no detach method at all (sio.c:193-238), so only pccard exposes this race family.