โฌข DragonFlyBSD Kernel Audit
DF-3727 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3727 harness โ€” ciss_disable_adapter() operates on an UNINITIALIZED
 * STAILQ head (sys/dev/raid/ciss/ciss.c:3465) โ€” the first
 * ciss_enqueue_complete(cr, &qh) (== STAILQ_INSERT_TAIL, sys/sys/queue.h:306)
 * executes `*(head)->stqh_last = (elm)` through whatever garbage the stack
 * slot holds => wild kernel write of a kernel heap pointer (the ciss_request),
 * and the later ciss_dequeue_complete() dereferences the equally-garbage
 * stqh_first => wild read.
 *
 * DragonFly divergence: FreeBSD's ciss.c ciss_disable_adapter() has
 * STAILQ_INIT(&qh); (upstream ciss.c:3581) โ€” DragonFly's copy is missing it.
 *
 * This userspace replica proves the primitive with the EXACT queue.h macro
 * text and a stack-grooming step (a prior call paints the stack region the
 * callee's uninitialized `qh` will land on with a chosen address โ€” the
 * kernel-equivalent of prior callout/syscall activity leaving a controlled
 * value in that stack slot). If the first enqueue writes the request pointer
 * to the groomed (attacker-chosen) address, the wild-write primitive is proven.
 *
 * Build: cc -O2 -Wall -o harness harness.c        (build.sh also builds -O0)
 * Run:   ./harness
 * Fixed: cc -DFIXED ... exercises the STAILQ_INIT(&qh) patch (fix.diff).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>

/* ---- exact replica of DragonFly sys/sys/queue.h STAILQ (lines 288-291, 306-310) ---- */
struct ciss_request_rep {
	struct ciss_request_rep *cr_link;	/* STAILQ_ENTRY(ciss_request) cr_link */
	int			 cr_tag;
};
typedef struct cr_qhead {			/* typedef STAILQ_HEAD(, ciss_request) */
	struct ciss_request_rep *stqh_first;
	struct ciss_request_rep **stqh_last;
} cr_qhead_t;

#define STAILQ_INIT_REPLICA(head) do {					\
	(head)->stqh_first = NULL;					\
	(head)->stqh_last = &(head)->stqh_first;			\
} while (0)

/* sys/sys/queue.h:306-310 โ€” NOTE: no NULL guard on stqh_last */
#define STAILQ_INSERT_TAIL_REPLICA(head, elm) do {			\
	(elm)->cr_link = NULL;						\
	*(head)->stqh_last = (elm);					\
	(head)->stqh_last = &(elm)->cr_link;				\
} while (0)

/* watch page at a fixed "attacker-chosen" address */
#define WATCH_ADDR ((struct ciss_request_rep **)0x51000000UL)
#define WATCH_PAGE ((void *)0x51000000UL)

static struct ciss_request_rep reqs[4];

/*
 * Stack groomer: paints 64 longs (512 bytes) of its stack frame with
 * WATCH_ADDR, then returns. The immediately-following call at the same stack
 * depth reuses this painted region for its own locals โ€” including the
 * uninitialized `qh`.
 */
static void __attribute__((noinline)) groom(void)
{
	volatile unsigned long paint[64];
	int i;

	for (i = 0; i < 64; i++)
		paint[i] = (uintptr_t)WATCH_ADDR;
	/* force paint onto the stack */
	if (paint[63] == 0xdeadbeefUL)
		printf("(groom oddity)\n");
}

/*
 * Faithful replica of ciss_disable_adapter() (ciss.c:3462-3507), reduced to
 * the queue manipulation.  `nbusy` = number of BUSY requests found.
 */
static int __attribute__((noinline)) ciss_disable_adapter_rep(int nbusy)
{
	cr_qhead_t qh;				/* ciss.c:3465 โ€” NO STAILQ_INIT */
	struct ciss_request_rep *cr;
	int i, wild = 0;
	void *observed_last;
	void *observed_first;

#ifdef FIXED
	STAILQ_INIT_REPLICA(&qh);		/* the fix (FreeBSD ciss.c:3581) */
#endif
	/* ciss.c:3475-3484 */
	for (i = 0; i < nbusy; i++) {
		cr = &reqs[i];
		observed_last = (i == 0) ? (void *)qh.stqh_last : NULL;
		if (i == 0 && qh.stqh_last == WATCH_ADDR)
			wild = 1;		/* the uninitialized slot holds the groomed ptr */
		/* ciss.c:3483: ciss_enqueue_complete(cr, &qh) */
		STAILQ_INSERT_TAIL_REPLICA(&qh, cr);
	}
	observed_first = (void *)qh.stqh_first;	/* ciss.c:3487 dequeue reads this */

	/* ciss.c:3486-3488: ciss_dequeue_complete(sc, &qh) โ€” reads stqh_first
	 * (garbage if list never initialized properly) */
	printf("    first-enqueue stqh_last = %p (groomed target %p)%s\n",
	       observed_last, (void *)WATCH_ADDR,
	       wild ? "  <== UNINITIALIZED STACK SLOT" : "");
	printf("    post-enqueue stqh_first = %p (list integrity %s)\n",
	       observed_first,
	       (qh.stqh_first == &reqs[0] || qh.stqh_first == NULL) ? "ok" :
	       "BROKEN (garbage head)");
	(void)observed_first;
	return wild;
}

int main(void)
{
	void *page;
	int i, wild_hits = 0, runs = 3;

	page = mmap(WATCH_PAGE, 4096, PROT_READ | PROT_WRITE,
		    MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
	if (page != WATCH_PAGE) {
		perror("mmap watch page");
		return 2;
	}
	memset(page, 0, 4096);
	for (i = 0; i < 4; i++)
		reqs[i].cr_tag = i + 1;

	printf("== DF-3727 ciss_disable_adapter uninitialized STAILQ head ==\n");
	printf("scenario: adapter heartbeat failed 3x (ciss.c:3389-3398) with 2 BUSY\n"
	       "requests; ciss_periodic -> ciss_disable_adapter (ciss.c:3395);\n"
	       "stack slot holding qh.stqh_last groomed to %p by prior activity\n\n",
	       (void *)WATCH_ADDR);

	for (i = 0; i < runs; i++) {
		printf("run %d:\n", i + 1);
		memset(page, 0, 4096);		/* clear watch page */
		groom();			/* paint the stack */
		if (ciss_disable_adapter_rep(2))
			wild_hits++;
		/* did the first enqueue write a request pointer at WATCH_ADDR? */
		if (*(struct ciss_request_rep **)WATCH_PAGE == &reqs[0]) {
			printf("    [WILD WRITE] *(%p) = %p (kernel: request ptr)\n",
			       (void *)WATCH_ADDR, (void *)&reqs[0]);
		}
	}
	printf("\n");
	if (wild_hits > 0 &&
	    *(struct ciss_request_rep **)WATCH_PAGE == &reqs[0]) {
		printf("[BUG REPRODUCED] %d/%d runs: STAILQ_INSERT_TAIL wrote the\n"
		       "request pointer THROUGH the uninitialized stqh_last to the\n"
		       "attacker-groomed address %p. In the kernel this is an\n"
		       "uncontrolled write of a kernel heap pointer to a\n"
		       "stack-garbage-controlled address (CWE-457 -> CWE-787).\n",
		       wild_hits, runs, (void *)WATCH_ADDR);
		return 0;
	}
#ifdef FIXED
	printf("[FIXED] STAILQ_INIT(&qh) present: no write through garbage,\n"
	       "list head consistent, no watch-page hit.\n");
	return 0;
#else
	printf("[NOT REPRODUCED] grooming did not land on the qh slot in this\n"
	       "build (stack layout shifted); see harness.O0 (unoptimized build).\n");
	return 1;
#endif
}