/*
 * DF-4244 harness: arithmetic proof of the OOB walk in dpt_detect_cache()
 * sys/dev/raid/dpt/dpt_scsi.c:648-669 using the DPT_HCP_* macros
 * verbatim from sys/dev/raid/dpt/dpt.h:341-344.
 *
 * The controller is asked for a 512-byte log page (cp_datalen = htonl(512),
 * dpt_scsi.c:618) DMA'd into `buff` (&dccb[1], inside the init sg page).
 * The walk bound, however, is taken from the *device-fed* page header:
 *
 *      bytes = DPT_HCP_LENGTH(buff);          // dpt_scsi.c:648
 *      ...
 *      while (DPT_HCP_CODE(param) != 6) {
 *              param = DPT_HCP_NEXT(param);   // reads param[3]
 *              if (param < buff || param >= &buff[bytes])
 *                      return;                // bound = device-fed bytes
 *      }
 *
 * DPT_HCP_LENGTH(page) = ntohs(*(int16_t *)&page[2]) + 4  -> up to 65539,
 * NOT the 512 bytes actually transferred.  A hostile/faulty controller
 * that reports a large page length steers `param` up to ~64 KB past the
 * 512-byte DMA buffer; every step reads param[0..1] (code) and param[3]
 * (NEXT length) from kernel memory the device never wrote.
 *
 * This program replays the exact loop over a model of the kernel's init
 * sg page (4096 bytes, buff = first 512), counts reads that land outside
 * the 512 bytes the HBA DMA'd, and reports the farthest offset reached.
 *
 * Build:  cc -O2 -o dpt_df4244_walk dpt_df4244_walk.c
 * Run:    ./dpt_df4244_walk
 * Success criterion: walk proceeds past buff+512 while the
 * `param >= &buff[bytes]` check still passes; reads up to bytes(=65539)
 * are classified OOB w.r.t. the DMA transfer size.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* ---- verbatim from dpt.h:341-344 ---- */
#define DPT_HCP_LENGTH(page)	(ntohs(*(int16_t *)(void *)(&page[2]))+4)
#define DPT_HCP_FIRST(page) 	(&page[4])
#define DPT_HCP_NEXT(param) 	(&param[3 + param[3] + 1])
#define DPT_HCP_CODE(param)	(ntohs(*(int16_t *)(void *)param))
/* ------------------------------------- */

static uint16_t ntohs_(uint16_t v) { return __builtin_bswap16(v); }
#define ntohs ntohs_

int main(void)
{
	const unsigned DMA_LEN = 512;		/* cp_datalen, dpt_scsi.c:618 */
	const unsigned PAGE = 4096;		/* bus_dmamem_alloc'd sg page  */
	unsigned char *kern = calloc(1, 65536 + 4096); /* "kernel memory" model */
	unsigned char *buff = kern;		/* buff = &dccb[1] within page */
	int bytes, steps = 0;
	long oob_reads = 0, same_page_reads = 0, far_reads = 0;
	unsigned char *param, *maxp = buff;

	/* Hostile controller fills the 512-byte DMA buffer: */
	buff[0] = 0x33;			/* page code 0x33 (sanity check passes) */
	buff[2] = 0xFF; buff[3] = 0xFF;	/* page length 0xFFFF -> bytes = 65539 */
	*(uint16_t *)&buff[4] = ntohs_(1);	/* first param code = 1 (passes !=1) */
	buff[8] = 0x04;			/* param[4] & 0x4: cache possible      */
	buff[7] = 0;			/* param[3]=0 -> NEXT step = 4 bytes   */

	bytes = DPT_HCP_LENGTH(buff);
	printf("DF-4244 dpt_detect_cache() walk model\n");
	printf("device reports page length bytes=%d; actual DMA transfer=%u; page=%u\n",
	       bytes, DMA_LEN, PAGE);

	/* dpt_scsi.c:651-658 checks */
	if (DPT_HCP_CODE(DPT_HCP_FIRST(buff)) != 1) { printf("layout chk failed\n"); return 1; }

	/* dpt_scsi.c:663-669: the exact loop */
	param = DPT_HCP_FIRST(buff);
	while (DPT_HCP_CODE(param) != 6) {
		param = DPT_HCP_NEXT(param);
		if ((param < buff) || (param >= &buff[bytes]))
			break;			/* the only bound: device-fed bytes */
		steps++;
		if (param > maxp) maxp = param;
		/* classify the reads this iteration will do next (code + next len) */
		if (param >= buff + DMA_LEN) {
			oob_reads++;
			if (param < buff + PAGE) same_page_reads++;
			else far_reads++;
		}
		if (steps > 100000) break;	/* safety for the model */
	}

	printf("walk stopped after %d accepted steps at buff+%td (max)\n",
	       steps, maxp - buff);
	printf("reads OUTSIDE the %u-byte DMA buffer : %ld iterations\n",
	       DMA_LEN, oob_reads);
	printf("  of which past the whole 4096B sg page: %ld iterations "
	       "(arbitrary adjacent kernel memory)\n", far_reads);
	printf("bound used by driver : &buff[bytes] = buff+%d\n", bytes);
	printf("bound that is safe   : &buff[%u] (the DMA'd length)\n", DMA_LEN);

	if (oob_reads > 0 && bytes > (int)DMA_LEN) {
		printf("\nPROVEN: with a device-fed length of %d, the walk accepts\n"
		       "pointers up to buff+%d while only %u bytes were DMA'd ->\n"
		       "kernel OOB read of up to %d bytes past the transfer buffer.\n",
		       bytes, bytes, DMA_LEN, bytes - DMA_LEN);
		return 0;
	}
	printf("\nNOT PROVEN\n");
	return 1;
}
