โฌข DragonFlyBSD Kernel Audit
DF-4559 / df4559_harness.c
โ† back to finding โ†“ download raw
/*
 * DF-4559 harness โ€” proof of the sassc->targets[] OOB write in
 * sys/dev/raid/mps/mps_sas_lsi.c (mpssas_volume_add / IR-config REMOVED branch).
 *
 * Replicates, byte-faithfully on x86-64:
 *   - struct mpssas_target   (sys/dev/raid/mps/mps_sas.h:40-79)
 *   - the allocation         (sys/dev/raid/mps/mps_sas.c:696-697):
 *         sassc->targets = kmalloc(sizeof(struct mpssas_target) * sc->facts->MaxTargets)
 *   - the ID space           (sys/dev/raid/mps/mps_mapping.c:1733-1734):
 *         sc->max_volumes = facts->MaxVolumes;
 *         sc->max_devices = facts->MaxTargets + sc->max_volumes;
 *   - the IR volume maprange (sys/dev/raid/mps/mps_mapping.c:232-234, HIGH mode):
 *         start_idx = max_devices - max_volumes  (== MaxTargets)
 *         end_idx   = start_idx + max_volumes - 1 (== MaxTargets+MaxVolumes-1)
 *   - the write sequence of mpssas_volume_add (mps_sas_lsi.c:847-853):
 *         targ = &sassc->targets[id];
 *         targ->tid = id; targ->handle = handle; targ->devname = wwid;
 *         TAILQ_INIT(&targ->commands); TAILQ_INIT(&targ->timedout_commands);
 *         SLIST_INIT(&targ->luns);
 *   - the zeroing sequence of the IR REMOVED branch (mps_sas_lsi.c:310-316):
 *         targ->handle = 0; targ->encl_slot = 0; targ->encl_handle = 0;
 *         targ->exp_dev_handle = 0; targ->phy_num = 0; targ->linkrate = 0;
 *
 * Scenarios (each fork()ed so a SIGSEGV is contained and reported):
 *   voladd-HIGH : volume_add at id = max_devices - max_volumes (first volume
 *                 slot with IOC pg8 IRVolumeMappingFlags = HIGH) -> OOB write.
 *   voladd-LOW  : control, volume ids [0, max_volumes) -> in bounds, clean.
 *   removed-HIGH: REMOVED-branch zeroing at id = max_devices-1 -> OOB write.
 *   canary      : no guard; count corrupted bytes past the array end and show
 *                 the per-field clobber offsets (the fw-controlled u64
 *                 devname=wwid write included).
 *   info        : print sizeof/offsetof table + the u16-truncation corner
 *                 (MaxTargets+MaxVolumes > 0xFFFF) that makes ir_shutdown's
 *                 start_idx underflow (mps_sas_lsi.c:901) โ€” informational.
 *
 * Build: cc -O2 -o df4559 df4559_harness.c
 * Run:   ./df4559
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/queue.h>
#include <signal.h>
#include <errno.h>

/* ---- verbatim replica of struct mpssas_target (mps_sas.h:40-79) ---- */
struct mps_command;
struct mpssas_lun;
struct sysctl_oid;
struct sysctl_oid_entry;
TAILQ_HEAD(sysctl_ctx_list, sysctl_oid_entry);

struct mpssas_target {
	uint16_t	handle;
	uint8_t		linkrate;
	uint64_t	devname;
	uint32_t	devinfo;
	uint16_t	encl_handle;
	uint16_t	encl_slot;
	uint8_t		flags;
#define MPSSAS_TARGET_INREMOVAL	(1 << 3)
#define MPS_TARGET_FLAGS_RAID_COMPONENT	(1 << 4)
	uint16_t	tid;
	SLIST_HEAD(, mpssas_lun) luns;
	TAILQ_HEAD(, mps_command) commands;
	struct mps_command *tm;
	TAILQ_HEAD(, mps_command) timedout_commands;
	uint16_t        exp_dev_handle;
	uint16_t        phy_num;
	uint64_t	sasaddr;
	uint16_t	parent_handle;
	uint64_t	parent_sasaddr;
	uint32_t	parent_devinfo;
	struct sysctl_ctx_list sysctl_ctx;
	struct sysctl_oid *sysctl_tree;	/* opaque ptr */
	TAILQ_ENTRY(mpssas_target) sysctl_link;
	uint64_t        issued;
	uint64_t        completed;
	unsigned int    outstanding;
	unsigned int    timeouts;
	unsigned int    aborts;
	unsigned int    logical_unit_resets;
	unsigned int    target_resets;
};

/* ---- replicated driver state (types per mpsvar.h:394,397; mpi2_ioc.h:275,282) ---- */
struct facts { uint16_t MaxTargets; uint8_t MaxVolumes; };

static size_t tgt_size;
static size_t n_alloc;		/* entries in sassc->targets  */
static uint32_t max_devices, max_volumes;

static void
layout_init(struct facts f)
{
	n_alloc     = f.MaxTargets;			/* mps_sas.c:697 */
	max_volumes = f.MaxVolumes;			/* mps_mapping.c:1733 */
	max_devices = (uint16_t)(f.MaxTargets + f.MaxVolumes); /* u16! :1734 */
	tgt_size    = sizeof(struct mpssas_target);
}

/* IR maprange, mps_mapping.c:220-235 */
static void
ir_maprange(int high, uint32_t *start, uint32_t *end)
{
	if (!high) {
		*start = 0;
	} else {
		*start = max_devices - max_volumes;	/* == MaxTargets */
	}
	*end = *start + max_volumes - 1;
}

/* place a "kmalloc(n_alloc * tgt_size)" so its END abuts a PROT_NONE page */
static struct mpssas_target *
guarded_targets(size_t *out_end_off)
{
	size_t ps = 4096, usable = n_alloc * tgt_size;
	size_t pages = (usable + ps - 1) / ps + 1;	/* +1 fully slack page */
	char *m = mmap(NULL, (pages + 1) * ps, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (m == MAP_FAILED) { perror("mmap"); exit(2); }
	if (mprotect(m + pages * ps, ps, PROT_NONE) != 0) { perror("mprotect"); exit(2); }
	*out_end_off = usable;
	/* array occupies [m + pages*ps - usable, m + pages*ps) */
	return (struct mpssas_target *)(m + pages * ps - usable);
}

/* mps_sas_lsi.c:847-853 โ€” mpssas_volume_add write sequence, verbatim math */
static void
volume_add_writes(struct mpssas_target *targets, uint32_t id,
    uint16_t handle, uint64_t wwid)
{
	struct mpssas_target *targ = &targets[id];	/* :847 */
	targ->tid = id;					/* :848 */
	targ->handle = handle;				/* :849 */
	targ->devname = wwid;				/* :850 */
	TAILQ_INIT(&targ->commands);			/* :851 */
	TAILQ_INIT(&targ->timedout_commands);		/* :852 */
	SLIST_INIT(&targ->luns);			/* :853 */
}

/* mps_sas_lsi.c:310-316 โ€” IR REMOVED branch, verbatim math */
static void
removed_zeroing(struct mpssas_target *targets, uint32_t id)
{
	struct mpssas_target *targ = &targets[id];	/* :310 */
	targ->handle = 0x0;
	targ->encl_slot = 0x0;
	targ->encl_handle = 0x0;
	targ->exp_dev_handle = 0x0;
	targ->phy_num = 0x0;
	targ->linkrate = 0x0;
}

static int run_child(void (*fn)(void), const char *name)
{
	pid_t p = fork();
	if (p == 0) { fn(); _exit(0); }
	int st; waitpid(p, &st, 0);
	if (WIFSIGNALED(st)) {
		printf("  [%s] -> KILLED by SIGSEGV at write past allocation end "
		       "(OOB write confirmed)\n", name);
		return 1;
	}
	if (WIFEXITED(st) && WEXITSTATUS(st) == 0) {
		printf("  [%s] -> clean exit (all writes in bounds)\n", name);
		return 0;
	}
	printf("  [%s] -> unexpected exit %d\n", name, WEXITSTATUS(st));
	return -1;
}

static struct facts F = { .MaxTargets = 256, .MaxVolumes = 8 }; /* SAS2008-IR-like */

static void sc_voladd_high(void)
{
	size_t end_off; struct mpssas_target *t = guarded_targets(&end_off);
	uint32_t s, e; ir_maprange(1, &s, &e);
	printf("    HIGH maprange [%u..%u], targets n=%zu (%zu bytes), "
	    "first volume write at byte %zu (past end at %zu)\n",
	    s, e, n_alloc, n_alloc * tgt_size, (size_t)s * tgt_size, end_off);
	volume_add_writes(t, s, 0x00fc, 0x4141414242424243ULL); /* id=MaxTargets */
	printf("    BUG: write completed without fault??\n");
}

static void sc_voladd_low(void)
{
	size_t end_off; struct mpssas_target *t = guarded_targets(&end_off);
	uint32_t s, e; ir_maprange(0, &s, &e);
	printf("    LOW maprange [%u..%u] (control), all < n=%zu\n", s, e, n_alloc);
	for (uint32_t id = s; id <= e; id++)
		volume_add_writes(t, id, 0x00fc, 0x4141414242424243ULL);
}

static void sc_removed_high(void)
{
	size_t end_off; struct mpssas_target *t = guarded_targets(&end_off);
	uint32_t s, e; ir_maprange(1, &s, &e);
	printf("    REMOVED zeroing at last volume slot id=%u -> byte %zu "
	    "(end at %zu)\n", e, (size_t)e * tgt_size, end_off);
	removed_zeroing(t, e);
	printf("    BUG: write completed without fault??\n");
}

/* fix model: sassc->targets sized sc->facts->MaxTargets + MaxVolumes
 * (== sc->max_devices), so every mapping-table id is in bounds. */
static void sc_voladd_high_fixed(void)
{
	size_t end_off; struct mpssas_target *t;
	uint32_t s, e;

	n_alloc = max_devices;		/* FIXED allocation size */
	t = guarded_targets(&end_off);
	ir_maprange(1, &s, &e);
	printf("    FIXED: n=%zu entries, HIGH maprange [%u..%u] all < n\n",
	    n_alloc, s, e);
	for (uint32_t id = s; id <= e; id++)
		volume_add_writes(t, id, 0x00fc, 0x4141414242424243ULL);
	n_alloc = F.MaxTargets;		/* restore for any later scenario */
}

static void sc_canary(void)
{
	/* no guard; measure the corruption footprint past the array end */
	size_t total = (n_alloc + 16) * tgt_size;
	struct mpssas_target *t = calloc(1, total);
	memset((char *)t + n_alloc * tgt_size, 0xAA,
	    total - n_alloc * tgt_size);
	uint32_t s, e; ir_maprange(1, &s, &e);
	for (uint32_t id = s; id <= e; id++) {
		volume_add_writes(t, id, 0x00fc, 0xdeadbeefcafe0609ULL);
		printf("    volume id %u: writes span bytes [%zu..%zu) "
		    "rel. to array base โ€” %s (offsetof devname=%zu, wwid u64 "
		    "lands at +%zu)\n",
		    id, (size_t)id * tgt_size, (size_t)(id + 1) * tgt_size,
		    (id >= n_alloc) ? "PAST END" : "in bounds",
		    offsetof(struct mpssas_target, devname),
		    (size_t)id * tgt_size + offsetof(struct mpssas_target, devname));
	}
	size_t first = SIZE_MAX, last = 0, cnt = 0;
	unsigned char *u = (unsigned char *)t;
	for (size_t i = n_alloc * tgt_size; i < total; i++) {
		if (u[i] != 0xAA) {
			if (first == SIZE_MAX) first = i;
			last = i; cnt++;
		}
	}
	printf("    canary result: %zu bytes clobbered past the allocation end, "
	    "spanning [+%zu .. +%zu] (allocation was %zu bytes)\n",
	    cnt, first, last, n_alloc * tgt_size);
	/* show the fw-controlled u64 landed where promised */
	uint64_t got = *(uint64_t *)((char *)t + s * tgt_size +
	    offsetof(struct mpssas_target, devname));
	printf("    devname readback at first volume slot: 0x%016jx "
	    "(expected 0xdeadbeefcafe0609) -> %s\n", (uintmax_t)got,
	    got == 0xdeadbeefcafe0609ULL ? "MATCH: fw-controlled 8B OOB write" : "MISMATCH");
	free(t);
}

static void sc_info(void)
{
	printf("  sizeof(struct mpssas_target) [replica, x86-64] = %zu\n", tgt_size);
	printf("  offsetof: handle=%zu linkrate=%zu devname=%zu tid=%zu "
	    "commands=%zu luns=%zu timedout=%zu\n",
	    offsetof(struct mpssas_target, handle),
	    offsetof(struct mpssas_target, linkrate),
	    offsetof(struct mpssas_target, devname),
	    offsetof(struct mpssas_target, tid),
	    offsetof(struct mpssas_target, commands),
	    offsetof(struct mpssas_target, luns),
	    offsetof(struct mpssas_target, timedout_commands));
	/* u16 truncation corner for ir_shutdown start_idx (mps_sas_lsi.c:901) */
	struct facts f2 = { .MaxTargets = 65535, .MaxVolumes = 255 };
	uint32_t md = (uint16_t)(f2.MaxTargets + f2.MaxVolumes);
	printf("  [informational] MaxTargets=65535 MaxVolumes=255 -> "
	    "sc->max_devices truncates to %u (< max_volumes=%u); "
	    "ir_shutdown start_idx = max_devices - max_volumes = %u (u32 wrap) "
	    "-> &sc->mapping_table[0x%x] wild read (mps_sas_lsi.c:901-905)\n",
	    md, f2.MaxVolumes, md - f2.MaxVolumes, md - f2.MaxVolumes);
}

int
main(void)
{
	printf("== DF-4559: sassc->targets[] OOB write (mps_sas_lsi.c volume "
	    "registration vs mps_sas.c:697 alloc) ==\n");
	layout_init(F);
	printf("facts: MaxTargets=%u MaxVolumes=%u -> targets n=%zu entries "
	    "(%zu B); ID space max_devices=%u (0..%u)\n\n",
	    F.MaxTargets, F.MaxVolumes, n_alloc, n_alloc * tgt_size,
	    max_devices, max_devices - 1);

	sc_info();
	printf("\n-- guarded (array end == guard page start) --\n");
	run_child(sc_voladd_high, "voladd-HIGH ");
	run_child(sc_voladd_low,  "voladd-LOW  ");
	run_child(sc_removed_high, "removed-HIGH");
	run_child(sc_voladd_high_fixed, "voladd-HIGH-FIXED");
	printf("\n-- canary (corruption footprint) --\n");
	sc_canary();
	return 0;
}