/*
 * DF-1557 — userspace harness for the unchecked PhysDiskMap OOB write in
 * mps_wd_config_pages() (sys/dev/raid/mps/mps_config.c:460-465).
 *
 * The guest has no LSI SAS HBA, so the in-kernel mps(4) driver never
 * attaches and the live code path cannot be triggered from userspace.
 * This harness reproduces the *exact* C logic of the buggy write loop
 * against a struct laid out identically to the tail of struct mps_softc
 * (DD_column_map[] as the final field, mpsvar.h:442), with a guard page
 * placed immediately after. A firmware PhysDisk entry with PhysDiskMap
 * >= MPS_MAX_DISKS_IN_VOL writes one controlled byte at a controlled
 * offset past the softc allocation and faults into the guard page —
 * proving the primitive.
 *
 * Layout reproduced verbatim from:
 *   - sys/dev/raid/mps/mpsvar.h:270-273   (struct mps_column_map)
 *   - sys/dev/raid/mps/mpsvar.h:442       (DD_column_map[MPS_MAX_DISKS_IN_VOL])
 *   - sys/dev/raid/mps/mpsvar.h:106       (MPS_MAX_DISKS_IN_VOL = 10)
 *   - sys/dev/raid/mps/mpi/mpi2_cnfg.h:1314-1321 (Mpi2RaidVol0PhysDisk_t)
 *   - sys/dev/raid/mps/mps_config.c:460-465 (the buggy loop)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>

#define MPS_MAX_DISKS_IN_VOL 10

/* sys/dev/raid/mps/mpsvar.h:270-273 — kernel struct is naturally 4 bytes */
struct mps_column_map {
    uint16_t dev_handle;
    uint8_t  phys_disk_num;
    /* 1 byte pad -> sizeof = 4 */
};

/* sys/dev/raid/mps/mpi/mpi2_cnfg.h:1314-1321 */
typedef struct {
    uint8_t RAIDSetNum;
    uint8_t PhysDiskMap;
    uint8_t PhysDiskNum;
    uint8_t Reserved;
} __attribute__((packed)) Mpi2RaidVol0PhysDisk_t;

/* Tail of struct mps_softc: DD_column_map is the LAST field (mpsvar.h:442).
 * For the harness we only need to model the tail accurately, because
 * DD_column_map[].phys_disk_num is what gets written. We place it as the
 * final member so anything past [MPS_MAX_DISKS_IN_VOL-1] is "past the softc".
 */
struct mps_softc_tail {
    uint8_t padding[64];  /* irrelevant fields before DD_column_map */
    struct mps_column_map DD_column_map[MPS_MAX_DISKS_IN_VOL];
};

static sigjmp_buf jb;
static volatile int got_sig = 0;
static void handler(int s) { got_sig = s; siglongjmp(jb, 1); }

int main(int argc, char **argv)
{
    long pagesz = sysconf(_SC_PAGESIZE);
    /* firmware-controlled: PhysDiskMap (index) and PhysDiskNum (value) */
    uint8_t want_map = (argc > 1) ? (uint8_t)atol(argv[1]) : 255;
    uint8_t want_num = (argc > 2) ? (uint8_t)atol(argv[2]) : 0x41;
    uint8_t num_phys = (argc > 3) ? (uint8_t)atol(argv[3]) : 1; /* <=8 passes gate */

    printf("== DF-1557 harness: PhysDiskMap OOB write in mps_wd_config_pages ==\n");
    printf("sizeof(struct mps_column_map)  = %zu (kernel: 4 w/ pad)\n",
           sizeof(struct mps_column_map));
    printf("MPS_MAX_DISKS_IN_VOL          = %d (kernel: 10)\n",
           MPS_MAX_DISKS_IN_VOL);
    printf("DD_column_map array bytes     = %zu\n",
           sizeof(((struct mps_softc_tail*)0)->DD_column_map));
    printf("firmware PhysDiskMap          = %u (U8, 0..255)\n", want_map);
    printf("firmware PhysDiskNum          = 0x%02x (written value)\n", want_num);
    printf("firmware NumPhysDisks         = %u (passes line-400 gate <=8)\n",
           num_phys);

    /* Layout: [DD_column_map][guard page] — DD_column_map tail must END exactly
     * at the guard page boundary, so any write past DD_column_map[9] faults.
     * Map 2 pages; place DD_column_map so its last byte touches the boundary.
     * (In the kernel, DD_column_map is the LAST field of struct mps_softc, so
     *  writes past its end land in whatever is allocated adjacent on the
     *  kernel heap — exactly the primitive we are demonstrating.) */
    size_t ddcmap_sz = sizeof(struct mps_column_map) * MPS_MAX_DISKS_IN_VOL;
    size_t maplen = pagesz * 2;
    char *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); return 2; }
    char *guard_page = base + pagesz;
    if (mprotect(guard_page, pagesz, PROT_NONE) != 0) {
        perror("mprotect"); return 2;
    }
    /* sc points at a struct whose DD_column_map is positioned to end at base+pagesz */
    struct mps_softc_tail dummy;
    size_t tail_ddoffset = (char *)&dummy.DD_column_map - (char *)&dummy;
    /* position the struct so its DD_column_map ends at the guard page */
    char *sc_raw = guard_page - tail_ddoffset - ddcmap_sz;
    struct mps_softc_tail *sc = (struct mps_softc_tail *)sc_raw;
    memset(sc, 0xCC, tail_ddoffset + ddcmap_sz);

    /* Construct a RAID Volume Page 0 with NumPhysDisks PhysDisk entries.
     * The line-400 gate (NumPhysDisks > 8) is satisfied. */
    Mpi2RaidVol0PhysDisk_t rvpd[8];
    memset(rvpd, 0, sizeof(rvpd));
    for (int i = 0; i < num_phys && i < 8; i++) {
        rvpd[i].PhysDiskMap = want_map;
        rvpd[i].PhysDiskNum = want_num;
    }

    /* Reproduce the EXACT kernel code at mps_config.c:460-465:
     *   pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk;
     *   for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) {
     *       sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num =
     *           pRVPD->PhysDiskNum;
     *       pRVPD++;
     *   }
     */
    struct sigaction sa = {0};
    sa.sa_handler = handler;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);

    uint32_t byte_offset = (uint32_t)want_map * sizeof(struct mps_column_map)
                           + offsetof(struct mps_column_map, phys_disk_num);
    uint32_t past_end = (byte_offset >= sizeof(sc->DD_column_map))
                          ? byte_offset - (uint32_t)sizeof(sc->DD_column_map)
                          : 0;
    printf("\nwrite target: &DD_column_map[%u].phys_disk_num "
           "= byte offset %u from DD_column_map base\n",
           want_map, byte_offset);
    if (past_end)
        printf("-> %u bytes PAST end of DD_column_map (= past end of "
               "struct mps_softc, since DD_column_map is the final field)\n",
               past_end);

    if (want_map < MPS_MAX_DISKS_IN_VOL) {
        printf("PhysDiskMap=%u < MPS_MAX_DISKS_IN_VOL=%d: in-bounds write, "
               "no OOB.\n(use: %s <PhysDiskMap> [num] [cnt]  with map>=10)\n",
               want_map, MPS_MAX_DISKS_IN_VOL, argv[0]);
        sc->DD_column_map[want_map].phys_disk_num = want_num;
        printf("wrote in-bounds: DD_column_map[%u].phys_disk_num = 0x%02x\n",
               want_map, want_num);
        munmap(base, maplen);
        return 0;
    }

    printf("[BUGGY] running loop: sc->DD_column_map[%u].phys_disk_num = "
           "0x%02x ...\n", want_map, want_num);
    if (sigsetjmp(jb, 1) == 0) {
        /* Unchecked index, exactly as in the kernel. */
        Mpi2RaidVol0PhysDisk_t *pRVPD = rvpd;
        for (int index = 0; index < num_phys; index++) {
            sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num =
                pRVPD->PhysDiskNum;
            pRVPD++;
        }
        printf("!!! BUG NOT REPRODUCED — write happened without fault? "
               "(guard page missed)\n");
        munmap(base, maplen);
        return 3;
    }
    printf(">>> SIGSEGV/%d caught: controlled-byte OOB write past "
           "DD_column_map[%d] confirmed.\n", got_sig, MPS_MAX_DISKS_IN_VOL-1);
    printf(">>> PRIMITIVE CONFIRMED: a malicious HBA returning a RAID Volume "
           "Page 0 with PhysDiskMap=%u writes byte 0x%02x at offset +%u past "
           "the end of struct mps_softc.\n", want_map, want_num, past_end);
    printf(">>> This is the exact code at sys/dev/raid/mps/mps_config.c:462 "
           "(no bounds check on PhysDiskMap).\n");

    munmap(base, maplen);
    return 0;
}
