โฌข DragonFlyBSD Kernel Audit
DF-4834 / df4834_harness.c
โ† back to finding โ†“ download raw
/*
 * DF-4834 harness โ€” mrsas_passthru() raw-user-pointer validation->use TOCTOU
 *                        OOB write past the 16-entry stack arrays.
 *
 * sys/dev/raid/mrsas/mrsas.c:1151 hands the RAW userland a_data pointer to
 * mrsas_passthru(); mrsas_ioctl.c then:
 *   :186  validates `user_ioc->sge_count > MAX_IOCTL_SGE(16)` โ€” reads USER mem
 *   :225  `for (i = 0; i < user_ioc->sge_count; i++)`            โ€” RE-reads USER mem
 *         ... bus_dma_tag_create(..., &ioctl_data_tag[i])        โ€” writes stack[i]
 *   :307  copyout loop re-reads sge_count/sgl[] a second time (after the
 *         multi-millisecond firmware command โ€” a huge window)
 *   :354  cleanup loop re-reads a third time -> bus_dmamap_unload(tag[i])
 *
 * This harness replicates the struct layout (mrsas_ioctl.h:80-93, packed),
 * the four stack arrays and the verbatim loop of :225-264, with the arrays
 * placed just below a guard page (overflow in the kernel runs toward the
 * caller frames / saved registers / return address of mrsas_passthru).
 *
 * Arms:
 *   race   โ€” second thread flips sge_count 16 -> 0x20000 right after the
 *            kernel passed the :186 check (pipe handshake, deterministic)
 *   window2 โ€” deterministic: flip during the (mocked) firmware command;
 *            copyout loop (:307) then reads ioctl_data_mem[i] past the array
 *   fixed  โ€” kernel-copy model (copyin at ioctl entry, as in fix.diff):
 *            racer writes are invisible, loops stay bounded -> clean exit
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <pthread.h>
#include <sys/mman.h>

#define MAX_IOCTL_SGE 16
#define MEGAMFI_RAW_FRAME_SIZE 128

#pragma pack(1)
struct mrsas_iocpacket_h {                 /* mrsas_ioctl.h:80-92 verbatim */
    uint16_t host_no;
    uint16_t __pad1;
    uint32_t sgl_off;
    uint32_t sge_count;
    uint32_t sense_off;
    uint32_t sense_len;
    union {
        uint8_t raw[MEGAMFI_RAW_FRAME_SIZE];
        struct { uint8_t cmd, sense_len, cmd_status, scsi_status; } hdr;
    } frame;
    struct { void *iov_base; size_t iov_len; } sgl[MAX_IOCTL_SGE];
};
#pragma pack()

static long g_tag_allocs;
static int mock_tag_create(void **dmat) {   /* kmalloc'd tag lands in *dmat */
    *dmat = (void *)(0xdead000000000000UL + (__sync_fetch_and_add(&g_tag_allocs, 1) + 1));
    return 0;
}

static void *g_region;                       /* arrays + canary + guard      */
#define ZONE     4096
#define GUARDOFF (2 * ZONE)                  /* tag array | mem array | guard */

static sigjmp_buf jmp;
static volatile sig_atomic_t g_hit_guard;
static void onsegv(int sig) { (void)sig; g_hit_guard = 1; siglongjmp(jmp, 1); }

/* shared "user memory" that the racing thread mutates while kernel reads it */
static struct mrsas_iocpacket_h *u_ioc;      /* stands for the userland arg   */
static int p2c[2], c2p[2];                   /* handshake pipes               */

/* the verbatim shape of mrsas_ioctl.c:225-264.
 * volatile stands in for the kernel reality that every loop iteration calls
 * opaque externs (bus_dma_tag_create/bus_dmamem_alloc/copyin) which force
 * the compiler to re-read *user_ioc from (user) memory each time. */
static void passthru_loop(struct mrsas_iocpacket_h *ioc_, void *tagarr) {
    volatile struct mrsas_iocpacket_h *user_ioc = ioc_;
    long i;
    for (i = 0; i < (long)user_ioc->sge_count; i++) {   /* :225 RE-reads user mem */
        if (!user_ioc->sgl[i].iov_len) continue;        /* :226 */
        /* bus_dma_tag_create(..., &ioctl_data_tag[i]) :229-240 */
        mock_tag_create((void **)((char *)tagarr + 0 * ZONE + i * 8));
        /* bus_dmamem_alloc/bus_dmamap_load populate mem[i]/phys[i] :241-255 */
        *(void **)((char *)tagarr + 1 * ZONE + i * 8) = (void *)(0xcafe0000UL + i);
    }
}

static void *racer(void *arg) {
    long flip_to = (long)(intptr_t)arg;
    char b = 'x';
    (void)read(p2c[0], &b, 1);              /* wait until kernel passed :186 */
    u_ioc->sge_count = (uint32_t)flip_to;   /* TOCTOU flip in USER memory */
    (void)write(c2p[1], &b, 1);
    return NULL;
}

static int run_race(long flip_to) {
    pthread_t th;
    int rc;
    char b;
    g_hit_guard = 0;
    memset(g_region, 0, GUARDOFF);
    u_ioc->sge_count = 16;                  /* passes the :186 validation */

    rc = pthread_create(&th, NULL, racer, (void *)(intptr_t)flip_to);
    if (rc != 0) { printf("  pthread_create failed: %d\n", rc); exit(2); }

    signal(SIGSEGV, onsegv);
    signal(SIGBUS, onsegv);
    if (sigsetjmp(jmp, 1) == 0) {
        /* :186 */ if (u_ioc->sge_count > MAX_IOCTL_SGE) { printf("  rejected\n"); return -1; }
        (void)write(p2c[1], &b, 1);         /* racer fires NOW */
        (void)read(c2p[0], &b, 1);          /* flip is in */
        passthru_loop(u_ioc, g_region);
        pthread_join(th, NULL);
        return 0;                           /* no fault this run */
    }
    pthread_join(th, NULL);
    return 1;                               /* faulted = OOB write confirmed */
}

static void report_canary(void) {
    long i, first = -1, last = -1;
    for (i = 16; i < GUARDOFF / 8; i++) {
        uint64_t v = *(uint64_t *)((char *)g_region + i * 8);
        if (v && first < 0) first = i;
        if (v) last = i;
    }
    if (first < 0) { printf("  canary: untouched\n"); return; }
    printf("  canary: first OOB slot=%ld (byte +%ld past the 16-entry arrays), "
           "last=%ld\n", first, (first - 16) * 8, last);
}

/* window-2 arm: copyout loop (:307) reading ioctl_data_mem[i] past the array
 * after the count flipped during the (mocked) firmware command */
static void run_window2(void) {
    memset(g_region, 0, GUARDOFF);
    for (int i = 0; i < 16; i++)
        *(void **)((char *)g_region + 1 * ZONE + i * 8) = (void *)(0xcafe0000UL + i);
    /* past-array slots hold "stack garbage" โ€” model adjacent-frame values */
    for (int i = 16; i < 24; i++)
        *(void **)((char *)g_region + 1 * ZONE + i * 8) = (void *)(0xfffff00012345678UL + i);

    u_ioc->sge_count = 16;                  /* validated at :186 */
    usleep(2000);                           /* :301 firmware command runs ~ms */
    u_ioc->sge_count = 24;                  /* raced during the command */
    printf("  copyout loop (:307) with raced sge_count=%u:\n", u_ioc->sge_count);
    for (long i = 16; i < (long)u_ioc->sge_count; i++) {
        void *src = *(void **)((char *)g_region + 1 * ZONE + i * 8);
        printf("    copyout(ioctl_data_mem[%ld] = %p  /* kernel stack value */,"
               " user iov_base, iov_len)\n", i, src);
    }
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);

    if (pipe(p2c) || pipe(c2p)) { perror("pipe"); return 2; }

    g_region = mmap(NULL, GUARDOFF + 4096, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANON, -1, 0);
    mprotect((char *)g_region + GUARDOFF, 4096, PROT_NONE);

    u_ioc = mmap(NULL, sizeof(*u_ioc) + 65536, PROT_READ | PROT_WRITE,
                 MAP_SHARED | MAP_ANON, -1, 0);
    memset(u_ioc, 0, sizeof(*u_ioc) + 65536);
    for (int i = 0; i < MAX_IOCTL_SGE + 64; i++)
        u_ioc->sgl[i].iov_len = 128;
    u_ioc->frame.hdr.cmd = 0x03;            /* MFI_CMD_LD_SCSI_IO */

    printf("sizeof(struct mrsas_iocpacket)=%zu (kernel packed: 400)\n",
           sizeof(struct mrsas_iocpacket_h));

    printf("[arm 1] race at the validation->use boundary (:186 check vs :225 loop)\n");
    int r = run_race(0x20000);
    if (r == 1)
        printf("  -> SIGSEGV at guard page: ioctl_data_tag[i]/ioctl_data_mem[i] "
               "written OUT OF BOUNDS, i ran toward 0x20000\n");
    else if (r == 0)
        printf("  -> loop completed without fault (flip lost)\n");
    report_canary();

    printf("\n[arm 2] wide window: flip during firmware command; "
           "copyout loop (:307) โ€” deterministic\n");
    run_window2();

    printf("\n[arm 3] FIXED model: kernel copy of the packet (copyin at entry)\n");
    {
        struct mrsas_iocpacket_h kcopy;
        memcpy(&kcopy, u_ioc, sizeof(kcopy));
        kcopy.sge_count = 16;
        u_ioc->sge_count = 0x20000;         /* racer flips USER memory only */
        memset(g_region, 0, GUARDOFF);
        passthru_loop(&kcopy, g_region);
        int touched = 0;
        /* past-end of tag array (page 0, bytes >= 16*8) and of the mem
         * array (page 1, bytes >= ZONE+16*8); guard page untouched = clean */
        for (long off = 16 * 8; off < ZONE; off += 8)
            if (*(uint64_t *)((char *)g_region + off)) touched = 1;
        for (long off = ZONE + 16 * 8; off < GUARDOFF; off += 8)
            if (*(uint64_t *)((char *)g_region + off)) touched = 1;
        if (*(uint64_t *)((char *)g_region + 16 * 8 - 8) == 0) {
            printf("  internal error: array slot [15] not written\n"); return 2;
        }
        printf("  loops stayed in [0,16): clean exit; writes past arrays: %s\n",
               touched ? "SEEN (BUG)" : "none");
    }
    return 0;
}