โฌข DragonFlyBSD Kernel Audit
DF-5794 / df5794.c
โ† back to finding โ†“ download raw
/*
 * DF-5794 PoC โ€” ttyflush(FREAD|FWRITE) discards the VSTART (XON) character
 * queued by ttyunblock(), permanently wedging IXOFF flow control.
 *
 * Mechanism (sys/kern/tty.c ttyflush):
 *   rw&FREAD branch sees TS_TBLOCK set -> ttyunblock() queues VSTART (0x11)
 *   into t_outq (tty.c:1542) and clears TS_TBLOCK (tty.c:1540);
 *   the TRAILING `if (rw & FWRITE) FLUSHQ(&tp->t_outq)` (tty.c:1487-1488)
 *   then throws that character away.  A peer that honoured the earlier
 *   XOFF is never told to resume and the kernel believes it is unblocked
 *   (TS_TBLOCK cleared), so no later path re-sends the XON.
 *
 * Test rig: pty pair, slave in non-canonical mode with IXOFF.
 *   TEST    arm: fill slave input > t_ihiwat (512) -> kernel sends XOFF to
 *             master; ioctl(TIOCFLUSH, FREAD|FWRITE); drain master;
 *             watch for XON for 2s.  BUG => XON never arrives.
 *   CONTROL arm: same fill; slave READS the input down below t_ilowat
 *             (ttread out-path ttyunblock); XON must arrive at master.
 *
 * build: cc -O -o df5794 df5794.c
 * run:   ./df5794
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <poll.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

#define	XON	0x11
#define	XOFF	0x13

static int
read_byte(int fd, int timeout_ms, unsigned char *out)
{
	struct pollfd pfd;
	unsigned char b;
	int n;

	pfd.fd = fd;
	pfd.events = POLLIN;
	for (;;) {
		n = poll(&pfd, 1, timeout_ms);
		if (n < 0) {
			if (errno == EINTR)
				continue;
			return -1;
		}
		if (n == 0)
			return 0;		/* timeout */
		n = read(fd, &b, 1);
		if (n < 0) {
			if (errno == EINTR || errno == EAGAIN)
				continue;
			return -1;
		}
		if (n == 0)
			return 0;
		*out = b;
		return 1;
	}
}

/* drain everything pending on fd */
static void
drain(int fd)
{
	unsigned char buf[512];
	fcntl(fd, F_SETFL, O_NONBLOCK);
	while (read(fd, buf, sizeof(buf)) > 0)
		;
	fcntl(fd, F_SETFL, 0);
}

static int
make_pty(int *masterp, int *slavep)
{
	struct termios tio;
	int m, s;

	m = open("/dev/ptmx", O_RDWR | O_NOCTTY);
	if (m < 0) {
		perror("open /dev/ptmx");
		return -1;
	}
	if (grantpt(m) < 0 || unlockpt(m) < 0) {
		perror("grantpt/unlockpt");
		return -1;
	}
	s = open(ptsname(m), O_RDWR | O_NOCTTY);
	if (s < 0) {
		perror("open pts");
		return -1;
	}
	if (tcgetattr(s, &tio) < 0) {
		perror("tcgetattr");
		return -1;
	}
	cfmakeraw(&tio);
	tio.c_cflag |= CREAD;
	tio.c_iflag |= IXOFF;		/* kernel sends XOFF/XON for this side */
	tio.c_cc[VSTART] = XON;
	tio.c_cc[VSTOP] = XOFF;
	tio.c_cc[VMIN] = 1;
	tio.c_cc[VTIME] = 0;
	if (tcsetattr(s, TCSANOW, &tio) < 0) {
		perror("tcsetattr");
		return -1;
	}
	*masterp = m;
	*slavep = s;
	return 0;
}

/*
 * Fill slave input queue past t_ihiwat-3 (non-canon) so the kernel
 * flow-blocks the line and queues XOFF to the master.  Returns after the
 * XOFF has been observed on the master.
 */
static int
fill_and_get_xoff(int m, int s)
{
	unsigned char buf[768];
	unsigned char b;
	int i, n;

	memset(buf, 'A', sizeof(buf));
	n = write(m, buf, sizeof(buf));		/* master -> slave input */
	if (n != sizeof(buf)) {
		fprintf(stderr, "short master write %d\n", n);
		return -1;
	}
	usleep(300000);				/* let ttyinput run */

	/* the XOFF must come out of the master side */
	while (read_byte(m, 1000, &b) == 1) {
		if (b == XOFF)
			return 0;
		/* other bytes would be echo; raw mode has none */
	}
	return -1;
}

int
main(void)
{
	unsigned char b;
	int m, s, rc, i;
	int xon_seen;
	int fails = 0;
	int arg;

	setvbuf(stdout, NULL, _IONBF, 0);

	/* ---------------- CONTROL arm: read-drain must emit XON -------- */
	if (make_pty(&m, &s) < 0)
		return 2;
	if (fill_and_get_xoff(m, s) < 0) {
		printf("CONTROL: FAIL (no XOFF observed โ€” rig broken)\n");
		return 2;
	}
	printf("CONTROL: XOFF (0x13) received by master\n");

	/* slave reads all input -> ttread out-path -> ttyunblock -> XON */
	fcntl(s, F_SETFL, O_NONBLOCK);
	for (;;) {
		unsigned char rb[512];
		rc = read(s, rb, sizeof(rb));
		if (rc <= 0)
			break;
	}
	fcntl(s, F_SETFL, 0);

	xon_seen = 0;
	for (i = 0; i < 20; i++) {
		rc = read_byte(m, 100, &b);
		if (rc == 1 && b == XON) {
			xon_seen = 1;
			break;
		}
		if (rc < 0)
			break;
	}
	printf("CONTROL: XON (0x11) after slave read-drain: %s\n",
	       xon_seen ? "SEEN (expected)" : "NOT SEEN (unexpected!)");
	if (!xon_seen)
		fails++;
	close(s);
	close(m);

	/* ---------------- TEST arm: TIOCFLUSH discards the XON ---------- */
	if (make_pty(&m, &s) < 0)
		return 2;
	if (fill_and_get_xoff(m, s) < 0) {
		printf("TEST: FAIL (no XOFF observed โ€” rig broken)\n");
		return 2;
	}
	printf("TEST: XOFF (0x13) received by master; TS_TBLOCK set\n");

	drain(m);				/* clear any leftovers FIRST */
	arg = FREAD | FWRITE;			/* == tcflush(TCIOFLUSH) */
	if (ioctl(s, TIOCFLUSH, &arg) < 0) {
		perror("ioctl TIOCFLUSH");
		return 2;
	}
	printf("TEST: TIOCFLUSH(FREAD|FWRITE) done (master pre-drained)\n");

	/*
	 * The kernel queued an XON (ttyunblock at tty.c:1542) and the
	 * trailing FLUSHQ at tty.c:1488 discarded it.  Nothing will ever
	 * re-send it: TS_TBLOCK is already clear.  A peer that honours
	 * XOFF stays stopped forever.  Watch 2s for any XON.
	 */
	xon_seen = 0;
	for (i = 0; i < 20; i++) {
		rc = read_byte(m, 100, &b);
		if (rc == 1 && b == XON) {
			xon_seen = 1;
			break;
		}
		if (rc < 0)
			break;
	}
	printf("TEST: XON (0x11) after TCIOFLUSH: %s\n",
	       xon_seen ? "SEEN (bug NOT reproduced)" :
	                  "NOT SEEN => XON discarded by ttyflush (BUG)");

	/*
	 * Secondary: show the kernel believes the line is unblocked:
	 * more master->slave input flows fine, yet still no XON.
	 */
	{
		unsigned char wb[64], rb[64];
		memset(wb, 'B', sizeof(wb));
		if (write(m, wb, sizeof(wb)) == sizeof(wb)) {
			usleep(150000);
			fcntl(s, F_SETFL, O_NONBLOCK);
			rc = read(s, rb, sizeof(rb));
			fcntl(s, F_SETFL, 0);
			printf("TEST: post-flush input still flows "
			       "(read %d bytes) while resume char is lost\n", rc);
		}
	}
	if (xon_seen)
		fails++;

	close(s);
	close(m);

	printf("%s\n", fails ? "RESULT: BUG NOT REPRODUCED" :
			       "RESULT: BUG REPRODUCED (XON discarded by ttyflush)");
	return fails ? 1 : 0;
}