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

sioclose() gone-path double-free of com->ibuf: kfree guarded by '!= NULL' but pointer never NULLed β€” every further close on the detached unit re-frees the same M_DEVBUF block

Summary

sioclose()'s gone branch kfrees com->ibuf then bzeros the tty - com->ibuf is never NULLed and com->gone is sticky. After a pccard ejection with >=2 open fds on the unit (detach sets gone and deliberately leaves ibuf to sioclose), the first close frees the buffer and every subsequent sioclose passes the stale '!= NULL' check and kfrees the freed block again: deterministic double free, no race. The second close also runs comhardclose's MMIO outb()s through the released bus handle, and the bzero destroys a struct tty that com->tp/dev->si_tty still reference. Physical-hotplug class (PCMCIA serial ejection); M_DEVBUF block size (256..2048B) user-preselectable via baud rate -> heap corruption. Distinct from DF-2191. Live console-glue suite verified on the guest's sio0 console (REFUTED the DF-3530 l_rint-token cross-note: always under tty_token; timestamps zero-init; speed-lock merge; unpriv EACCES boundary). Guest lacks pccard - double-free honestly inconclusive, source-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3902 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
df3902_trigger.c β€” 1.6 KB view raw
sio_ioctl_test.c β€” 3.5 KB view raw
build.sh β€” 98 B view raw
run.sh β€” 239 B view raw
build.log β€” 45 B view raw
run.log β€” 294 B view raw
run_ioctl.log β€” 739 B view raw
run_user_boundary.log β€” 311 B view raw
env.txt β€” 419 B view raw
fix.diff β€” 477 B view raw
manifest.json β€” 944 B view raw
verdict.json β€” 2.8 KB view raw

DF-3902 β€” sioclose() gone-path double-free: com->ibuf kfree'd twice (no NULL-after-free) + live tty bzero'd

File: sys/dev/serial/sio/sio.c Class: double free (CWE-415) + object-lifetime hygiene on pccard hot-removal Severity: Low (requires PCMCIA/pccard sio device + hot ejection + β‰₯2 open fds; deterministic when triggered β€” no race needed) Related-but-distinct: DF-2191 (sio_pccard.c: dtr_ch/busy_ch callouts not stopped before softc free β€” UAF via callout). This finding is the other half: the double-free through repeated sioclose() in sio.c itself.

What

sioclose() (sio.c:1449) tail:

if (com->gone) {
    kprintf("sio%d: gone\n", com->unit);
    crit_enter();
    if (com->ibuf != NULL)
        kfree(com->ibuf, M_DEVBUF);      /* :1479 β€” NOT NULLed after free */
    bzero(tp, sizeof *tp);              /* :1480 β€” bzero of live struct tty */
    crit_exit();
}

The freeing branch is guarded only by com->ibuf != NULL, but the pointer is never cleared, and com->gone stays set, so every subsequent sioclose() on another still-open fd of the same unit re-frees the same M_DEVBUF block.

Trigger sequence (deterministic):

  1. Process A and B each hold an open fd on /dev/ttydN (or cuaaN) of a pccard-attached sio unit.
  2. Card is ejected β†’ sio_pccard_detach() (sio_pccard.c:118) sets com->gone = 1, tears down intr + ioport, and β€” because TS_ISOPEN β€” forces ttyclose(com->tp) without freeing ibuf (the "still open, forcing close" branch deliberately leaves ibuf to sioclose).
  3. A closes its fd β†’ sioclose β†’ gone-branch β†’ kfree(com->ibuf) (1st free), bzero(tp). com->ibuf still points at freed memory.
  4. B closes its fd β†’ sioclose β†’ gone-branch β†’ com->ibuf != NULL (dangling) β†’ kfree(com->ibuf) again β†’ double free of an M_DEVBUF block.

Aggravators in the same branch: * bzero(tp, sizeof *tp) destroys a struct tty that com->tp still references (clist state, termios) β€” any straggling tty-layer reference (blocked l_read woken later, dev->si_tty) then operates on a zeroed tty. * comhardclose() on the 2nd close still does outb() MMIO writes via the bus handle released by detach (sio_pccard.c:135).

Impact

Kernel heap corruption (double free of M_DEVBUF) on hardware with PCMCIA serial devices under card ejection. Requires physical hot-unplug (or driver-controller modunload path), so privilege is "physical access / hotplug event": Low severity, certain confidence β€” the sequence is deterministic, no race window involved.

Live verification performed (on this guest β€” console IS sio0)

The ioctl-glue and data-plane around the buggy path were exercised live (sio_ioctl_test.c, guest console = sio0): control-device termios ioctls, lock-merge enforcement, timestamp zero-init, TIOCMGET, 100KB TX burst β€” all behave exactly as source predicts; see run.log / run_user_boundary.log. The double-free itself needs a pccard device, which the guest lacks β†’ status inconclusive (source-certain), like DF-2191's guest half.

Fix

NULL com->ibuf after the kfree (and don't bzero a tty that com->tp still references) β€” see fix.diff.

VERDICT.md
↓ download raw

DF-3902 VERDICT

Status: inconclusive for the double-free itself (source-certain, pccard-gated); live verification performed for the surrounding ioctl/data-plane on the guest console (sio0) reproduced: 0 Β· impact: none (demonstrated class: heap corruption / panic on affected hardware) Β· confidence: certain

Why the double-free is certain from source

  1. sioclose() tail (sio.c:1475-1482): when com->gone, it runs if (com->ibuf != NULL) kfree(com->ibuf, M_DEVBUF); and then bzero(tp, sizeof *tp); β€” without setting com->ibuf = NULL and with com->tp still pointing at the now-zeroed tty.
  2. com->gone is sticky (only set by sio_pccard_detach, sio_pccard.c:127).
  3. sio_pccard_detach with TS_ISOPEN takes the "still open, forcing close" branch (sio_pccard.c:135-137): it calls ttyclose() (clearing TS_ISOPEN) and deliberately leaves ibuf for sioclose to free.
  4. With β‰₯2 fds open at ejection time, each fd close enters sioclose, passes the control-device early-out (non-control fds), reaches the gone branch, and the second close passes com->ibuf != NULL on the dangling pointer β†’ kfree() of an already-freed M_DEVBUF block (sio.c:1478-1479). Deterministic β€” no race, no window: the guard is the stale pointer itself.
  5. Additionally the 2nd close runs comhardclose() which issues outb() to com->modem_ctl_port etc. through the bus handle released by detach (sio_pccard.c:131-135), and bzero(tp) corrupts the still-referenced struct tty (com->tp, dev->si_tty).

Distinct from DF-2191 (that is the dtr_ch/busy_ch callout UAF in sio_pccard.c's own teardown; this is the repeated-sioclose double-free whose fix belongs in sio.c). Both are facets of the same missing-drain detach, but the defects and fixes are independent.

Live verification on the guest (console = sio0, 16550A)

  • df3902_trigger.c run against the live console /dev/ttyd0 as root: two opens + two closes, RC=0, no panic, guest healthy (run.log) β€” the non-gone close path is exercised live and behaves correctly (gone==0 so the free branch is never taken β€” exactly as source predicts).
  • sio_ioctl_test.c live on the console (run_ioctl.log):
  • TIOCTIMESTAMP/TIOCDCDTIMESTAMP return all-zero timevals (attach-time bzero holds; no stale-data leak from these ioctls),
  • TIOCMGET returns 0x167 (commctl DMGET correct),
  • console speed lock enforced: TIOCSETA(57600) accepted (rc=0) but tp->t_ispeed stays 115200 β€” the merge block sio.c:2077-2099 silently restores locked fields, live-confirmed,
  • control devices (ttyid0): TIOCGETA/TIOCSETA/TIOCGWINSZ all as source predicts (TIOCGWINSZ zeroed; root TIOCSETA mutates it_in, restored).
  • Unprivileged boundary live (run_user_boundary.log): uid=1001 cannot open any of /dev/ttyd0 /dev/ttyid0 /dev/ttyld0 /dev/cuaa0 (EACCES; 0600/0660 nodes), so the in-ioctl caps_priv_check calls are correct second layer.
  • 100KB TX burst through the console tty: clean, guest stable.

Why not fully reproduced

The guest has no PCMCIA/pccard bridge or serial card, so com->gone can never be set on it; the double free cannot be triggered on this VM. Recorded inconclusive (source-certain), consistent with DF-2191's guest treatment.

Exploit chain

Physical-access class: insert pccard serial card, hold 2 fds, eject β†’ double free of an M_DEVBUF block whose size is attacker-influenced via the last set baud rate (siosetwater: 2*ibufsize, 256..2048 bytes) β†’ classic malloc-arena corruption β†’ potential code execution under the right heap state, at minimum a panic. Out of scope to weaponize for a hotplug-only, dead-hardware-class bug on this guest.

Fix validation

Not applicable (no reproduced corruption on guest). fix.diff NULLs com->ibuf after the kfree and skips the redundant bzero of a live tty.

kernel_refs

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.diff NULLs com->ibuf/com->iptr after the kfree and drops the bzero of the live tty; not build-validated (trigger not runnable on guest; no reproduced corruption gating mandatory validation)

fix.diff
↓ fix.diffper-fix-DF-3902

Confirmed kernel references

Detail

Exploit chain

on pccard hw: open ttydN twice -> eject card -> close fd1 -> close fd2 -> kfree of freed M_DEVBUF (size 2*ibufsize, 256..2048B attacker-influenced via prior baud change) -> heap corruption/panic; physical-hotplug class

Evidence (decisive lines)

VERDICT.md; df3902_trigger.c; run.log (guest live: clean non-gone path); run_ioctl.log + run_user_boundary.log (live ioctl-glue verification on sio0 console)

PoC changes

authored fresh (trigger + ioctl-glue suite)

Verified recommended fix

Set com->ibuf = NULL (and iptr) after the gone-path kfree; do not bzero the still-referenced struct tty

Verdict

Deterministic double-free, source-certain: sioclose()'s gone-branch (sio.c:1478-1479) kfrees com->ibuf under a '!= NULL' guard but never NULLs it, and com->gone is sticky; after a pccard ejection with >=2 open fds on the unit (sio_pccard_detach takes the 'still open, forcing close' branch and deliberately leaves ibuf to sioclose), every further sioclose re-kfrees the same M_DEVBUF block. Second close also runs comhardclose MMIO writes through the bus handle detach already released, and bzero(tp) destroys a struct tty that com->tp/dev->si_tty still reference. Distinct defect from DF-2191 (callout UAF, other file). Guest verification: trigger program + full ioctl-glue suite run live on the guest's sio0 console (two open/close cycle clean RC=0; TIOCTIMESTAMP zeros; TIOCMGET 0x167; console speed-lock merge live-proven; 100KB TX stable; unpriv EACCES on all nodes) β€” but the guest has no pccard hardware so com->gone can never be set: the double-free itself is honestly inconclusive.