โฌข DragonFlyBSD Kernel Audit
DF-4363 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-4363 โ€” mps_config_get_man_pg10: firmware-controlled DMA heap overflow.
 *
 * Kernel code mirrored exactly (sys/dev/raid/mps/mps_config.c):
 *   :249  cm->cm_length = le16toh(mpi_reply->Header.PageLength) * 4;
 *         (PageLength is U8 from the IOC config-page header reply -> max 255,
 *          so cm_length can be up to 1020 bytes)
 *   :254  page = kmalloc(MPS_MAN_PAGE10_SIZE, M_MPT2, M_ZERO | M_INTWAIT);
 *         (mpsvar.h:105  ->  #define MPS_MAN_PAGE10_SIZE 0x5C  == 92 bytes,
 *          the ONLY fixed-size page allocation in the whole file)
 *   :260  cm->cm_data = page;
 *   :261  error = mps_request_polled(sc, cm);   (DATAIN DMA of cm_length bytes)
 *
 * The DMA SGL is built from (cm_data, cm_length):
 *   sys/dev/raid/mps/mps.c:2099-2101
 *         bus_dmamap_load(sc->buffer_dmat, cm->cm_dmamap,
 *                         cm->cm_data, cm->cm_length, mps_data_cb, cm, 0);
 *   sys/dev/raid/mps/mps.c:2081ff mps_data_cb -> one SGE per loaded segment,
 *         total transfer length == cm->cm_length.
 * bus_dmamap_load does NOT know the malloc size โ€” it happily maps 1020 bytes
 * starting at a 92-byte allocation, and the IOC then DMAs the whole
 * Manufacturing Page 10 (PageLength dwords, content chosen by the firmware)
 * past the end of the 92-byte heap object.
 *
 * Every sibling getter in this file (and every getter in the newer
 * sys/dev/raid/mpr/mpr_config.c) allocates cm->cm_length instead:
 *   mps_config.c:136, 607, 730, 852, 970, 1089, 1209, 1354
 *   mpr_config.c:145, 277, 450, 588, 724, 861, 998, 1130, 1262, 1394, 1552
 *
 * Harness method (same as the DF-1556/DF-1557 precedent): we cannot attach a
 * malicious WarpDrive HBA to the audit guest, so we reproduce the exact
 * kernel arithmetic and the exact write pattern in an unprivileged userland
 * process. The 92-byte "kmalloc" is placed at the very end of a page that is
 * followed by a PROT_NONE guard page; the firmware DMA is modeled as a
 * memcpy() of cm_length bytes of firmware-chosen data. Any PageLength >= 24
 * (24*4 = 96 > 92) writes past the allocation and faults on the guard page.
 *
 * Compile:  cc -O2 -Wall -Wextra -o harness harness.c
 * Run:      ./harness <PageLength 1..255>
 * FIXED variant (models fix.diff): cc -DFIXED ... ; overflow must be gone.
 */

#include <sys/types.h>
#include <sys/mman.h>
#include <signal.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* --- verbatim constants from the kernel tree --- */
#define MPS_MAN_PAGE10_SIZE 0x5C        /* sys/dev/raid/mps/mpsvar.h:105 */
#define MPS_WD_LSI_OEM      0x80        /* mpsvar.h:99 */
#define MPS_WD_HIDE_EXPOSE_MASK 0x03    /* mpsvar.h:100 */

/* Mpi2ManufacturingPagePS_t (mpi2_cnfg.h:722-727):
 *   U8 PageType, PageNumber, PageVersion, PageLength;   // 0x00 header
 *   U32 ProductSpecificInfo;                            // 0x04 (used as [] array)
 * kernel derefs reach: page->ProductSpecificInfo (0x04) and pPS_info[5]
 * (0x04 + 5*4 = 0x18..0x1C) โ€” both inside 0x5C, so the field reads are fine;
 * the bug is purely the DMA length vs allocation size. */

static sigjmp_buf jmp;
static volatile sig_atomic_t got_sig;

static void
handler(int sig)
{
	got_sig = sig;
	siglongjmp(jmp, 1);
}

int
main(int argc, char **argv)
{
	long ps_arg;
	u_int page_length;	/* firmware PageLength (dwords) */
	u_int cm_length;	/* == cm->cm_length at mps_config.c:249/131-style */
	unsigned char *alloc92;	/* == page at mps_config.c:254 */
	unsigned char fw_page[255 * 4];	/* the "DMA" source, fw-chosen bytes */
	long ps = sysconf(_SC_PAGESIZE);
	void *map;

	if (argc != 2)
		exit(2);
	ps_arg = strtol(argv[1], NULL, 0);
	if (ps_arg < 1 || ps_arg > 255)
		exit(2);
	page_length = (u_int)ps_arg;

	printf("== DF-4363 harness: mps_config_get_man_pg10 DMA heap overflow ==\n");
	printf("kernel MPS_MAN_PAGE10_SIZE     = 0x%02X (%d)  (mpsvar.h:105)\n",
	    MPS_MAN_PAGE10_SIZE, MPS_MAN_PAGE10_SIZE);
	printf("firmware Manufacturing Pg10 PageLength = %u dwords\n", page_length);

	/* :249 */ cm_length = page_length * 4;
	printf("-> cm->cm_length (PageLength*4) = %u bytes  (mps_config.c:249)\n",
	    cm_length);
#ifdef FIXED
	{
		size_t fixed_sz = cm_length > MPS_MAN_PAGE10_SIZE ?
		    cm_length : MPS_MAN_PAGE10_SIZE;
		printf("[FIXED] kmalloc sized MAX(cm_length, 0x5C) = %zu"
		    " + length floor guard (fix.diff)\n", fixed_sz);
		if (cm_length < 0x1C) {
			printf("[FIXED] page shorter than ProductSpecificInfo[5]"
			    " needs -> EINVAL (fix.diff hunk 2)\n");
			exit(0);
		}
	}
#endif

	/* kmalloc(0x5C) with a guard page immediately after it */
	map = mmap(NULL, (size_t)(2 * ps), PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (map == MAP_FAILED) { perror("mmap"); exit(2); }
	if (mprotect((char *)map + ps, (size_t)ps, PROT_NONE) != 0) {
		perror("mprotect"); exit(2);
	}
	alloc92 = (unsigned char *)map + ps - MPS_MAN_PAGE10_SIZE;

	/* firmware-chosen page image: OEM byte at 0x04, pattern elsewhere */
	memset(fw_page, 0xA5, sizeof(fw_page));
	fw_page[4] = MPS_WD_LSI_OEM;
	fw_page[0x18] = 0x03;	/* pPS_info[5] hide/expose value */

	got_sig = 0;
	signal(SIGSEGV, handler);
	signal(SIGBUS, handler);

	if (sigsetjmp(jmp, 1) == 0) {
		if (cm_length > MPS_MAN_PAGE10_SIZE)
			printf("[BUGGY] IOC DMA of %u bytes into the 92-byte"
			    " page (mps.c:2099-2101 dmamap_load of"
			    " cm_data/cm_length) ...\n", cm_length);
		else
			printf("[OK] IOC DMA of %u bytes fits the 92-byte"
			    " page exactly\n", cm_length);
		fflush(stdout);

		/* the device write itself: mps_data_cb SGEs cover cm_length */
#ifdef FIXED
		memcpy(alloc92, fw_page,
		    cm_length > MPS_MAN_PAGE10_SIZE ? MPS_MAN_PAGE10_SIZE :
		    cm_length);
		/* (fixed allocation is >= cm_length, so full-length copy would
		 * also be safe; truncated copy models the conservative fix) */
#else
		memcpy(alloc92, fw_page, cm_length);
#endif

		/* kernel-side consumers (mps_config.c:286,297-299) */
		printf("page->ProductSpecificInfo OEM = 0x%02X, "
		    "pPS_info[5] hide/expose = 0x%02X\n",
		    alloc92[4], alloc92[0x18]);
	} else {
		printf(">>> SIGSEGV/%d caught: IOC DMA wrote past the 92-byte"
		    " heap allocation (guard page hit)\n", got_sig);
		printf(">>> PRIMITIVE CONFIRMED: firmware PageLength=%u"
		    " (cm_length=%u) overflows the 0x5C MPS_MAN_PAGE10_SIZE"
		    " buffer by %u bytes.\n", page_length, cm_length,
		    cm_length - MPS_MAN_PAGE10_SIZE);
		printf(">>> This is the exact code at"
		    " sys/dev/raid/mps/mps_config.c:249,254,260"
		    " (mps.c:2099-2101 builds the DMA over cm_length).\n");
		exit(1);
	}

	printf("no overflow: DMA stayed inside the allocation\n");
	return (0);
}