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)
PoC verification
Evidence pack
findings/poc/DF-2556 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
/
* 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
/
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
fixedVALIDATED. 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.
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).
No comments yet.