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

clist_alloc_cblocks leaks previous c_data buffer on every reallocation

Summary

clist_alloc_cblocks kmallocs new c_data copies old contents then overwrites cl->c_data without ever kfreeing previous buffer. Every reallocation to different non-zero size leaks old M_TTYS allocation. Leaky caller is ttsetwater invoked on each TIOCSETA ioctl. On speed-dependent drivers (sio sets t_ispeedwat=t_ospeedwat=(speed_t)-1) changing baud rate changes computed clist sizes so each termios update leaks prior t_rawq and t_outq buffers. Unprivileged local user with write access to serial device (dialout group) can exhaust kernel memory. clist_free_cblocks is only place that ever frees c_data ttsetwater never calls it before reallocating.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2556 Β· 14 files
FileTypeDescriptionSize
leak_mod.c trigger-source diagnostic kld: clist_alloc_cblocks resize loop -> measures M_TTYS leak 1.8 KB view raw
poc.c trigger-source userspace TIOCSETA baud-rate alternation against a sio tty (guest-limited: console locks speed) 3.1 KB view raw
README.md readme explains bug, live trigger, guest limitation, primitive-confirmation approach 2.1 KB ↓ raw
build_mod.sh build-script kld build via Makefile.leak / bsd.kmod.mk 340 B view raw
run_mod.sh run-script kldload + vmstat -m before/after 380 B view raw
build.sh build-script cc -O0 -o poc poc.c 118 B view raw
run.sh run-script userspace TIOCSETA loop + vmstat -m 329 B view raw
run.log run-log unpatched leak demo: 37/143K -> 19.6K/332M 197 B view raw
fix_build.log build-log nativekernel rebuild tail (NK_DONE rc=0) 268 B view raw
fix_run.log fix-validation patched #1: M_TTYS 37/143K -> 37/143K (flat) 511 B view raw
fix_results.txt fix-validation before/after contrast unpatched vs patched 511 B view raw
env.txt environment uname + cc version + patched kern.version 280 B view raw
fix.diff suggested-fix kfree old c_data before reassigning in clist_alloc_cblocks 482 B view raw
VERDICT.md verdict full narrative 4.1 KB ↓ raw
README.md readme explains bug, live trigger, guest limitation, primitive-confirmation approach
↓ download raw

/ * DF-2556 PoC β€” clist_alloc_cblocks() leaks the previous c_data buffer. * * Bug (sys/kern/tty_subr.c:48-81): * clist_alloc_cblocks() kmalloc()s a new data buffer (line 61), copies the * old contents, then overwrites cl->c_data = data (line 80) WITHOUT ever * kfree()ing the previous cl->c_data. Every reallocation to a different * non-zero size therefore leaks one M_TTYS allocation. The ccmax==0 path * correctly calls clist_free_cblocks(); the ccmax==c_ccmax path returns early; * only the "resize to a different size" path leaks. * * Live userspace trigger: TIOCSETA on a sio(4) serial tty (the only driver that * sets t_ispeedwat/t_ospeedwat = (speed_t)-1, sys/dev/serial/sio/sio.c:2480), * so that a baud-rate change actually changes the computed clist size * (tty.c:1085 ttsetwater -> tty.c:2457/2489 clist_alloc_cblocks). A user logged * in on such a serial port can leak kernel memory per baud-rate change. * * GUEST LIMITATION: this QEMU guest has exactly one sio tty, /dev/ttyd0, which * is the kernel console (comconsole). The console subsystem locks its speed * (verified: stty -f /dev/ttyd0 9600 returns silently but speed still * reports 115200; tcsetattr returns 0 but tp->t_ospeed is unchanged), so the * clist size never changes via TIOCSETA and no leak can be induced from * userspace on this guest. There is no second/non-console serial port. * * Therefore this file demonstrates the leak PRIMITIVE at the function level: * a tiny diagnostic module (leak_mod.c) calls clist_alloc_cblocks() directly * with alternating sizes and we measure M_TTYS growth. On the unpatched kernel * M_TTYS grows monotonically (each resize leaks the old buffer); on the fixed * kernel (kfree of old c_data) it stays flat. This is the same kind of * function-level primitive confirmation used for harness-only findings. * * Files: * leak_mod.c - kernel module: clist_alloc_cblocks resize loop (the harness) * build_mod.sh / run_mod.sh - build & run the module (root: kldload) * poc.c - the (guest-limited) live userspace trigger against a sio tty /

VERDICT.md verdict full narrative
↓ download raw

DF-2556 β€” clist_alloc_cblocks() leaks the previous c_data buffer

Verdict: REPRODUCED (memory leak / DoS); fix_status = fixed

Bug

clist_alloc_cblocks() (sys/kern/tty_subr.c:48-81) kmalloc()s a new data buffer (line 61), copies the old contents, then overwrites cl->c_data = data (line 80) without ever kfree()ing the previous cl->c_data. Every reallocation to a different non-zero size therefore leaks one M_TTYS allocation. (The ccmax==0 path correctly calls clist_free_cblocks() at :58; the ccmax==c_ccmax path returns early at :56; only the resize path leaks.)

Live trigger (code-level, unambiguous)

tty.c:1085 ttsetwater(tp) is called from the TIOCSETA handler; it calls clist_alloc_cblocks(&t_rawq, x) (:2457) and clist_alloc_cblocks(&t_outq, x) (:2489) where x depends on the baud rate. The clist sizes only vary with baud rate on a driver that sets t_ispeedwat/t_ospeedwat = (speed_t)-1, because then cps = speed/10 (ttsetwater :2439/2467) and the computed x changes. The only in-tree driver that does this is sio (sys/dev/serial/sio/sio.c:2480-2481, inside the input-buffer-resize path siosetitm). A user logged in on a sio serial port who repeatedly changes the baud rate via TIOCSETA/stty leaks kernel memory on every change β€” an unprivileged memory-exhaustion DoS.

Guest limitation (why a userspace PoC can't fire here)

This QEMU guest has exactly one sio tty, /dev/ttyd0, which is the kernel console (comconsole). The console subsystem locks its baud rate: verified stty -f /dev/ttyd0 9600 and stty -f /dev/ttyd0 50 return silently but stty -f /dev/ttyd0 speed still reports 115200; a C tcsetattr(B300) returns rc=0 but tcgetattr readback is still 115200. So the clist size never changes via TIOCSETA on the console, and no leak can be induced from userspace on this guest. There is no second / non-console sio port. (For a pty the leak also cannot fire: pty leaves speedwat=0, which makes both t_rawq and t_outq sizes baud-rate-independent β€” imax(ohiwat, TTMAXHIWAT) clamps the output size to a constant, and the input side uses a constant TTYHOG-512.)

Reproduction (primitive confirmed at function level)

Because the live userspace trigger needs hardware this guest lacks, the leak is demonstrated with a tiny diagnostic module (leak_mod.c) that calls clist_alloc_cblocks() directly with alternating sizes (1024 ↔ 16384 shorts) for 20000 cycles and measures M_TTYS growth. This is function-level primitive confirmation (same approach used for harness-only findings).

Unpatched #0 kernel:

M_TTYS BEFORE: 37 allocs / 143K
kldload df2556_leak.ko (20000 resize cycles)
M_TTYS AFTER:  19.6K allocs / 332M

~20000 leaked M_TTYS buffers = 332 MB of leaked kernel memory from 20000 resizes. The leak is unbounded (scales with resize count).

Fix

fix.diff adds a kfree() of the previous cl->c_data before reassigning, in clist_alloc_cblocks() (sys/kern/tty_subr.c, guarded for the first allocation where c_data == NULL):

    if (cl->c_data != NULL)
        kfree(cl->c_data, M_TTYS);
    cl->c_data = data;

Fix validation (full nativekernel rebuild β€” tty_subr.c is built-in)

Built make -j6 nativekernel KERNCONF=X86_64_GENERIC (rc=0), installed kernel.stripped β†’ /boot/kernel/kernel (verified valid ELF, new BuildID b32b089c, size identical 15705800), rebooted to #1 (Sun Aug 9 05:09:04 UTC 2026). Re-ran the SAME diagnostic module:

PATCHED #1: M_TTYS BEFORE: 37 allocs / 143K
            kldload df2556_leak.ko (20000 resize cycles)
            M_TTYS AFTER:  37 allocs / 143K   (FLAT)

Leak eliminated β€” identical workload, M_TTYS growth gone. fix_status=fixed.

Impact

Unbounded kernel memory leak (M_TTYS) β†’ local memory-exhaustion DoS. Triggerable unprivileged by any user with access to a sio serial tty (real serial terminal / modem / console-server port) by repeatedly changing the baud rate; on this guest the only sio tty is the locked console. Not a memory-corruption primitive (no attacker-shaped write) β€” it is a pure resource leak. Medium severity appropriate.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. tty_subr.c built into kernel, so full make -j6 nativekernel KERNCONF=X86_64_GENERIC built (rc=0), kernel.stripped installed to /boot/kernel/kernel (valid ELF, same size 15705800, new BuildID), rebooted to #1. Re-ran SAME diagnostic module: unpatched #0 grew M_TTYS 37/143K -> 19.6K/332M; patched #1 stayed 37/143K -> 37/143K (FLAT). kfree of old c_data eliminates leak under identical workload.

baseline #0: M_TTYS 37/143K -> 19.6K/332M (leak). patched #1: M_TTYS 37/143K -> 37/143K (no leak). Same 20000-cycle clist resize harness; growth eliminated.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Aug 9 05:09:04 UTC 2026 (full make -j6 nativekernel rebuild with tty_subr.c fix; kernel.stripped installed, BuildID b32b089c)

Confirmed kernel references

Detail

Exploit chain

none β€” pure memory leak (resource exhaustion DoS), not memory corruption; no write primitive, no escalation.

Evidence (decisive lines)

Unpatched #0: M_TTYS BEFORE 37 allocs/143K ; kldload df2556_leak.ko (20000 cycles sizeA=1024 sizeB=16384) ; M_TTYS AFTER 19.6K allocs/332M. Patched #1 (kfree old c_data added): M_TTYS BEFORE 37/143K ; AFTER 37/143K (FLAT).

PoC changes

Authored leak_mod.c (diagnostic kld that directly exercises clist_alloc_cblocks with size changes and measures M_TTYS) because live userspace trigger (sio TIOCSETA) needs hardware this guest lacks. poc.c is guest-limited userspace TIOCSETA alternation. fix.diff adds kfree(cl->c_data, M_TTYS) before reassigning, guarded for first allocation where c_data==NULL.

Verified recommended fix

In clist_alloc_cblocks() (sys/kern/tty_subr.c) free previous cl->c_data before dropping reference: before 'cl->c_data = data;' add 'if (cl->c_data != NULL) kfree(cl->c_data, M_TTYS);'. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-2556/fix.diff.

Verdict

REPRODUCED (unbounded kernel memory leak of M_TTYS). clist_alloc_cblocks() (tty_subr.c:48-81) kmalloc()s a new c_data (line 61), copies old contents, then overwrites cl->c_data = data (line 80) WITHOUT kfree()ing the previous cl->c_data β€” every reallocation to a different non-zero size leaks one M_TTYS buffer. Code bug unambiguous and certain. Live userspace trigger: TIOCSETA on a sio(4) serial tty (only driver that sets t_ispeedwat/t_ospeedwat=(speed_t)-1, sio.c:2480) so baud-rate change changes clist size via ttsetwater. GUEST LIMITATION: only sio tty here is /dev/ttyd0, kernel console, whose baud rate LOCKED by console subsystem (verified: stty -f /dev/ttyd0 9600 returns silently but speed still reports 115200); pty cannot trigger it (speedwat=0 makes both rawq/outq sizes baud-independent via imax(ohiwat,TTMAXHIWAT)). Leak confirmed at function level with diagnostic kld (leak_mod.c) calling clist_alloc_cblocks() directly with alternating sizes: unpatched M_TTYS grew 37/143K -> 19.6K/332M (20000 leaked buffers = 332MB).