Memory leak in clist_alloc_cblocks: old c_data never freed on resize
Summary
clist_alloc_cblocks(:61) kmalloc new data, bcopy from old(:71-74), cl->c_data=data(:80) WITHOUT kfree of old. Every ccmax change leaks old_ccmax*sizeof(short). Reachable: unpriv pty open+tcsetattr varying baud -> ttywatermarks -> clist_alloc_cblocks. Sustained -> kmem exhaustion DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0207 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0207_leak.c | trigger-source | userspace pty baud-toggle trigger (note: pty path does not change ccmax - see VERDICT) | 2.6 KB | view raw |
| df0207_harness.c | exploit-harness | kld that calls clist_alloc_cblocks directly with changing ccmax - definitive leak proof | 1.6 KB | view raw |
| Makefile | build | kld Makefile | 113 B | β download |
| build.sh | build-script | build userspace + kld | 864 B | view raw |
| run.sh | run-script | load harness, measure M_TTYS | 767 B | view raw |
| leak_sample.txt | leak-sample | vmstat -m before/after showing 143K->29.4M leak | 979 B | view raw |
| fix_build.log | build-log | single-fix kernel build output (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | FIXED kernel: M_TTYS stays 143K (no leak) | 883 B | view raw |
| env.txt | environment | uname, cc version | 229 B | view raw |
| fix.diff | suggested-fix | kfree old c_data before overwrite | 611 B | view raw |
| VERDICT.md | verdict | full root-cause + reachability analysis | 4.1 KB | β raw |
| README.md | readme | human reproduce doc | 821 B | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0207 β Memory leak in clist_alloc_cblocks (missing kfree of old c_data)
See VERDICT.md for the full analysis (root cause, proof, reachability nuance).
Reproduce (the leak)
./build.sh # builds the userspace trigger + the kld harness
sudo ./run.sh # kldload harness; vmstat -m | grep ttys before/after
UNPATCHED kernel: M_TTYS MemUse jumps 143K -> ~29M (leak). FIXED kernel: M_TTYS MemUse stays 143K (flat, no leak).
The harness (df0207_harness.c) calls clist_alloc_cblocks() directly with a changing ccmax, which is the definitive proof the bug leaks on every resize.
Fix
fix.diff adds kfree(odata, M_TTYS) of the old buffer before overwriting
cl->c_data. Validated: single-fix kernel #1 boots; harness no longer leaks
(M_TTYS flat). See fix_run.log.
DF-0207 β Memory leak in clist_alloc_cblocks() (missing kfree of old c_data)
Verdict: REPRODUCED (leak primitive definitively demonstrated via direct invocation).
Impact: leak / DoS β unbounded M_TTYS kernel-memory exhaustion. Not memory corruption,
so there is no privilege-escalation chain; the ceiling is kmem-exhaustion DoS.
The bug (root cause)
sys/kern/tty_subr.c, clist_alloc_cblocks():
48: void
49: clist_alloc_cblocks(struct clist *cl, int ccmax)
50: {
51: short *data;
...
61: data = kmalloc(ccmax * sizeof(*data), M_TTYS, M_INTWAIT|M_ZERO);
...
71: bcopy(cl->c_data + cl->c_cchead, data, n * sizeof(*data)); /* copy OLD -> new */
...
80: cl->c_data = data; /* <-- overwrites the OLD pointer WITHOUT kfree() */
81: }
Compare with clist_free_cblocks() (lines 86-100) which does kfree(data, M_TTYS).
Every time ccmax != cl->c_ccmax (a resize), the previous c_data buffer is orphaned β
a permanent leak of old_ccmax * sizeof(short) bytes of M_TTYS.
Proof (direct harness)
Because the finding's claimed unprivileged vector (pty + tcsetattr baud change) does
not actually change ccmax on this guest (see Reachability note below), a kernel-module
harness (df0207_harness.c) calls clist_alloc_cblocks() directly with an alternating ccmax
(1024 β 4096), 3000 iterations. Measured with vmstat -m | grep ttys:
| Count | MemUse | Requests | |
|---|---|---|---|
| BEFORE load | 37 | 143K | 70 |
| AFTER load | 5.90K | 29.4M | 5.93K |
~5860 c_data buffers orphaned, ~29 MB of M_TTYS permanently leaked. Each subsequent
clist_alloc_cblocks(cl, X) where X != cl->c_ccmax leaks again β unbounded.
Reachability (important nuance)
- The bug fires whenever a clist's
ccmaxchanges. Callers that resize:tty.c:1664/2435/2457/2489(ttsetwater/ttypend), netgraphng_tty/ng_h4,if_sl(SLIP). - The finding's stated trigger is partly incorrect: for a pty,
t_ispeedwat/t_ospeedwatare0(never set by the pty driver), sottsetwater()'scase 0:makes the computed outq/rawq buffer sizes constant regardless of baud (outq ccmax is alwaysimax(ohiwat,2048)+OBUFSIZ+100; ohiwat clamps to β€2048 for any speed). The early returnif (ccmax == cl->c_ccmax) return;then prevents any realloc, sotcsetattrbaud changes on a pty do not leak. - The only in-tree driver that sets
t_*speedwat = (speed_t)-1(the speed-dependent case that does vary ccmax with baud) issio.c:2480-2481(the COM serial driver). On this guest/dev/ttyd0(sio0) isroot:wheel 0600β not openable by an unprivileged user β and it is the live console, so its baud-change path could not be driven from userspace here. - Conclusion: the leak is real and unbounded on any code path that resizes a clist. The
realistic trigger on a stock system is a serial/tty driver or netgraph/SLIP path that varies
ccmax; the unprivileged-pty+baud vector claimed by the finding does not hold. Reported as a confirmed leak (DoS-class), reachability = needs a ccmax-change path (sio baud / driver / netgraph), not a blanket unprivileged trigger.
The fix
fix.diff saves the old c_data pointer (odata) before overwriting cl->c_data, and
kfree(odata, M_TTYS)s it afterward (guarding NULL). Minimal, targeted, mirrors
clist_free_cblocks.
Fix validation
Built a single-fix kernel (DF-0207 fix only) on the with-src baseline:
- Unpatched #0 baseline: harness load β M_TTYS 143K β 29.4M (leak).
- Patched kernel: harness load β M_TTYS stays flat (~143K, only the legitimately-live
buffers) β leak eliminated.
(See run.log / fix_run.log for the before/after vmstat -m lines.)
Kernel refs
sys/kern/tty_subr.c:61βkmallocof newdatasys/kern/tty_subr.c:80βcl->c_data = datawithoutkfree(the leak)sys/kern/tty_subr.c:99βclist_free_cblockscorrectlykfrees (reference)sys/kern/tty.c:2489βttsetwateroutq resize call sitesys/dev/serial/sio/sio.c:2480β only driver settingt_*speedwat = -1
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (kld+harness). clist_alloc_cblocks old c_data never freed -> 29MB M_TTYS leak. kld harness proven.
No comments yet.