# 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()`:

```c
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 `ccmax` changes. Callers that resize:
  `tty.c:1664/2435/2457/2489` (`ttsetwater` / `ttypend`), netgraph `ng_tty`/`ng_h4`,
  `if_sl` (SLIP).
- **The finding's stated trigger is partly incorrect**: for a pty, `t_ispeedwat`/`t_ospeedwat`
  are `0` (never set by the pty driver), so `ttsetwater()`'s `case 0:` makes the computed
  outq/rawq buffer sizes **constant** regardless of baud (outq ccmax is always
  `imax(ohiwat,2048)+OBUFSIZ+100`; ohiwat clamps to ≤2048 for any speed). The early return
  `if (ccmax == cl->c_ccmax) return;` then prevents any realloc, so `tcsetattr` baud 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) is **`sio.c:2480-2481`** (the COM serial driver). On this guest
  `/dev/ttyd0` (sio0) is `root: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` — `kmalloc` of new `data`
- `sys/kern/tty_subr.c:80` — `cl->c_data = data` **without** `kfree` (the leak)
- `sys/kern/tty_subr.c:99` — `clist_free_cblocks` correctly `kfree`s (reference)
- `sys/kern/tty.c:2489` — `ttsetwater` outq resize call site
- `sys/dev/serial/sio/sio.c:2480` — only driver setting `t_*speedwat = -1`
