โฌข DragonFlyBSD Kernel Audit
DF-3729 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3729 harness โ€” ciss_notify_rescan_logical() (sys/dev/raid/ciss/ciss.c:
 * 3851-3914) walks the controller-supplied REPORT_LOGICAL_LUNS reply with
 * NO bound on ndrives (ciss.c:3866 โ€” unlike ciss_init_logical, which checks
 * `ndrives > CISS_MAX_LOGICAL` at ciss.c:1357) and NO bounds check on
 * CISS_LUN_TO_BUS()/CISS_LUN_TO_TARGET() before indexing
 * sc->ciss_logical[bus][target] (ciss.c:3899-3901).
 *
 * Consequences with a malicious/buggy controller reply:
 *  - cll->lun[i] is read past the end of the kmalloc'd 15-LUN report buffer
 *    (ciss.c:3899) โ€” heap OOB READ feeding the index computation;
 *  - sc->ciss_logical[bus][target] with bus in [0,255] (array rows:
 *    ciss_max_logical_bus, typically 1-2) and target in [0,255] (row length:
 *    CISS_MAX_LOGICAL=15) โ€” heap OOB WRITE at ciss.c:3906-3908
 *    (ld->cl_update=0, ld->cl_address, ld->cl_controller) and inside
 *    ciss_identify_logical (ld->cl_ldrive / ld->cl_status pointer stores).
 *
 * Same code exists in FreeBSD (upstream-inherited); the pass-1 audit fixed
 * only the attach-time sibling (DF-1190, ciss_init_logical), not the
 * notify/rescan sibling.
 *
 * Build: cc -O2 -Wall -o harness harness.c
 * Fixed: cc -DFIXED ... clamps ndrives and validates bus/target (fix.diff).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* ---- real kernel constants (cissreg.h / cissvar.h) ---- */
#define CISS_MAX_LOGICAL	15			/* cissvar.h:49 */
#define CISS_LUN_TO_BUS(x)	(((x) >> 16) & 0xFF)	/* cissreg.h:509 */
#define CISS_LUN_TO_TARGET(x)	((x) & 0xFF)		/* cissreg.h:510 */

#define REDZONE 4096
#define POISON 0xAACCEEFFUL

struct ciss_lun_report_rep {			/* cissreg.h:172-177 */
	uint32_t	list_size;		/* big-endian */
	uint32_t	pad;
	uint32_t	lun[];			/* 8-byte addresses; low 32 used */
};

struct ldrive_rep {				/* struct ciss_ldrive (index fields) */
	uint64_t	cl_address;
	void		*cl_controller;
	int		cl_status;
	int		cl_update;
};

static int max_logical_bus = 2;			/* typical sc->ciss_max_logical_bus */

static struct ldrive_rep **
alloc_logical(void)
{
	struct ldrive_rep **rows;
	int i;

	rows = calloc(max_logical_bus, sizeof(*rows));
	for (i = 0; i < max_logical_bus; i++)
		rows[i] = calloc(CISS_MAX_LOGICAL, sizeof(struct ldrive_rep));
	return rows;
}

static uint32_t ntohl_(uint32_t x);			/* big-endian field on x86 */

int main(void)
{
	struct ciss_lun_report_rep *cll;
	struct ldrive_rep **logical;
	size_t report_bytes = sizeof(*cll) + CISS_MAX_LOGICAL * 8;	/* ciss.c:1270 */
	uint32_t fw_list_size;
	int ndrives, i, oob_reads = 0, oob_writes = 0, iters;

	printf("== DF-3729 ciss_notify_rescan_logical unbounded walk ==\n");
	printf("report buffer = %zu bytes (15 LUNs) + %d-byte redzone;"
	       " ciss_max_logical_bus=%d\n\n", report_bytes, REDZONE,
	       max_logical_bus);

	/* kmalloc'd reply buffer with an unmapped-style redzone behind it */
	cll = calloc(1, report_bytes + REDZONE);
	memset((char *)cll + report_bytes, POISON, REDZONE);
	logical = alloc_logical();

	/*
	 * Malicious controller data (written by the adapter into the DMA
	 * buffer): a valid first LUN, then a list_size claiming far more
	 * entries than the buffer holds.  The firmware also simply being
	 * buggy (returning garbage list_size) reaches the same state.
	 */
	cll->lun[0] = (0u << 16) | 3u;			/* bus 0, target 3 (valid) */
	cll->lun[1] = (1u << 16) | 7u;			/* bus 1, target 7 (valid) */
	fw_list_size = 0x00001000;			/* claims 4096 LUNs */

	/* ciss.c:3866 โ€” no CISS_MAX_LOGICAL sanity check here */
	ndrives = (int)(ntohl_(fw_list_size) / 8);
	printf("controller list_size=0x%x -> ndrives=%d (buffer holds %d;"
	       " init_logical would have rejected this at ciss.c:1357)\n\n",
	       fw_list_size, ndrives, CISS_MAX_LOGICAL);

#ifdef FIXED
	/* the fix: clamp like init_logical + validate bus/target */
	if (ndrives > CISS_MAX_LOGICAL) {
		printf("[fix] clamping ndrives %d -> %d\n\n", ndrives,
		       CISS_MAX_LOGICAL);
		ndrives = CISS_MAX_LOGICAL;
	}
#endif

	iters = ndrives > 256 ? 256 : ndrives;		/* cap demo runtime */
	for (i = 0; i < iters; i++) {
		int bus, target;
		struct ldrive_rep *ld;
		int past_buffer = (size_t)((char *)&cll->lun[i] -
					   (char *)cll) >= report_bytes;

		/* ciss.c:3899-3900 */
		bus = CISS_LUN_TO_BUS(cll->lun[i]);	/* redzone poison when
							 * i >= 15 */
		target = CISS_LUN_TO_TARGET(cll->lun[i]);

		if (past_buffer)
			oob_reads++;
		if (i < 8 || past_buffer) {
			printf("  i=%3d lun read %s (raw 0x%08x) -> bus=%3d"
			       " target=%3d", i,
			       past_buffer ? "OOB (redzone)" : "in-buffer",
			       cll->lun[i], bus, target);
			if (bus >= max_logical_bus || target >= CISS_MAX_LOGICAL)
				printf("  -> INDEXES ROW %d OF %d, COL beyond %d"
				       " (OOB WRITE)", bus, max_logical_bus,
				       CISS_MAX_LOGICAL - 1);
			printf("\n");
		}
		if (bus >= max_logical_bus || target >= CISS_MAX_LOGICAL) {
			oob_writes++;
#ifdef FIXED
			continue;	/* the fix rejects the entry outright */
#else
			continue;	/* the kernel would write through the
					 * OOB ld here (3906-3908 + identify) */
#endif
		}
		ld = &logical[bus][target];
		ld->cl_update = 0;			/* ciss.c:3906 */
		ld->cl_address = cll->lun[i];		/* ciss.c:3907 */
		ld->cl_controller = NULL;		/* ciss.c:3908 */
	}

	printf("\n");
	if (oob_reads > 0 || oob_writes > 0) {
		printf("[BUG REPRODUCED] %d OOB heap READS past the report"
		       " buffer and %d OOB row/col\n"
		       "index computations -> ciss_logical[bus][target] OOB"
		       " WRITES (cl_update,\ncl_address, cl_controller, then"
		       " identify stores cl_ldrive/cl_lstatus pointers).\n",
		       oob_reads, oob_writes);
		return 0;
	}
	printf("[FIXED] ndrives clamped to %d and bus/target validated:"
	       " 0 OOB accesses.\n", CISS_MAX_LOGICAL);
	return 0;
}

static uint32_t ntohl_(uint32_t x)			/* big-endian field on x86 */
{
	return ((x & 0xff) << 24) | ((x & 0xff00) << 8) |
	       ((x >> 8) & 0xff00) | (x >> 24);
}