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)
PoC verification
Evidence pack
findings/poc/DF-3902 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
- Process A and B each hold an open fd on
/dev/ttydN(or cuaaN) of a pccard-attached sio unit. - Card is ejected β
sio_pccard_detach()(sio_pccard.c:118) setscom->gone = 1, tears down intr + ioport, and β because TS_ISOPEN β forcesttyclose(com->tp)without freeingibuf(the "still open, forcing close" branch deliberately leaves ibuf to sioclose). - A closes its fd β sioclose β gone-branch β
kfree(com->ibuf)(1st free),bzero(tp).com->ibufstill points at freed memory. - 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.
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
sioclose()tail (sio.c:1475-1482): whencom->gone, it runsif (com->ibuf != NULL) kfree(com->ibuf, M_DEVBUF);and thenbzero(tp, sizeof *tp);β without settingcom->ibuf = NULLand withcom->tpstill pointing at the now-zeroed tty.com->goneis sticky (only set by sio_pccard_detach, sio_pccard.c:127).sio_pccard_detachwith TS_ISOPEN takes the "still open, forcing close" branch (sio_pccard.c:135-137): it callsttyclose()(clearing TS_ISOPEN) and deliberately leavesibuffor sioclose to free.- 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 != NULLon 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. - Additionally the 2nd close runs
comhardclose()which issuesoutb()tocom->modem_ctl_portetc. through the bus handle released by detach (sio_pccard.c:131-135), andbzero(tp)corrupts the still-referencedstruct 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.crun against the live console/dev/ttyd0as 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.clive 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
- sys/dev/serial/sio/sio.c:1475-1482 (gone branch: kfree without NULL + bzero tp)
- sys/dev/serial/sio/sio.c:1449-1485 (sioclose)
- sys/dev/serial/sio/sio_pccard.c:127 (gone=1), :135-137 (force-close leaves ibuf to sioclose)
- sys/dev/serial/sio/sio.c:1488-1545 (comhardclose does MMIO on released handle on 2nd close)
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.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
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.
No comments yet.