/*
 * DF-1869 — userspace harness port of scsi_decap() ISCSI_READ_DATA path.
 *
 * Faithfully replays sys/dev/disk/iscsi/initiator/iscsi_subr.c:566-574 in
 * userspace. Proves:
 *   - The only bounds check at L566 is `ntohl(cmd->edtlen) >= pq->pdu.ds_len`,
 *     i.e. it validates ONLY the segment length, never offset+len vs edtlen.
 *   - `offset = ntohl(rcmd->bo)` is fully attacker-controlled (bo is u_int,
 *     iscsi.h:256; cast to signed `int offset`, so high-bit bo sign-extends).
 *   - `dp = csio->data_ptr + offset` has NO bounds check; i_mbufcopy then
 *     writes `len = ds_len` attacker bytes at the attacker-chosen offset.
 *
 * The harness allocates a csio->data_ptr-equivalent buffer of EDTLEN bytes
 * surrounded by REDZONE canaries. It then runs the exact same arithmetic as
 * the kernel for a chosen attacker (bo, ds_len) and reports:
 *   - whether the (offset, offset+ds_len) write window is inside the buffer,
 *   - how many attacker bytes landed outside the buffer (OOB write),
 *   - in the negative-offset case, that dp lands BELOW data_ptr.
 *
 * Two builds of the same source:
 *   harness          — unfixed kernel logic (verbatim from iscsi_subr.c)
 *   harness_fixed    — same logic with the proposed fix (offset+len <= edtlen)
 *
 * Exit code 0 = no OOB write observed; non-zero = OOB write happened.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

/* ---- kernel-faithful types (from iscsi.h / iscsivar.h) ---- */
typedef uint32_t u_int;
typedef unsigned char u_char;
typedef char *      caddr_t;

struct mbuf {
    struct mbuf *m_next;
    int          m_len;
    char        *m_data;
};
#define mtod(m, ty) ((ty)((m)->m_data))

/* the buggy kernel inline, verbatim from iscsivar.h:565-583 */
static void i_mbufcopy(struct mbuf *mp, caddr_t dp, int len) {
    struct mbuf *m;
    caddr_t bp;
    for (m = mp; m != NULL; m = m->m_next) {
        bp = mtod(m, caddr_t);
        memcpy(dp, bp, (len < m->m_len ? len : m->m_len));
        dp += m->m_len;
        len -= m->m_len;
        if (len <= 0) break;
    }
}

#define RED 0xA5
#define REDZONE 4096   /* large enough to catch every test-case OOB write */

/*
 * Reproduce scsi_decap() ISCSI_READ_DATA branch.
 * Returns number of attacker bytes that landed outside [buf, buf+edtlen).
 */
static long run_decap(char *buf, long edtlen, long attacker_bo, long ds_len,
                      const char *attacker_bytes, int fixed) {
    /* Line 566: `if(ntohl(cmd->edtlen) >= pq->pdu.ds_len)` */
    if (!(edtlen >= ds_len)) {
        return -1;  /* kernel: xdebug("edtlen < ds_len"), falls through */
    }

    /* Line 567: `int offset, len = pq->pdu.ds_len;` */
    int offset;              /* signed int, like the kernel local */
    int len = (int)ds_len;

    /* PROPOSED FIX: bounds check the window against edtlen */
    if (fixed) {
        u_int uoffset = (u_int)attacker_bo;
        u_int ulen    = (u_int)ds_len;
        u_int uedtlen = (u_int)edtlen;
        if (uoffset > uedtlen || ulen > uedtlen - uoffset) {
            return -2;  /* fix rejects bad PDU */
        }
    }

    /* Line 572: `offset = ntohl(rcmd->bo);` */
    offset = (int)(u_int)attacker_bo;   /* kernel: signed int = u_int (impl-defined but usual 2's complement) */

    /* Line 573: `dp = csio->data_ptr + offset;` */
    caddr_t dp = buf + offset;

    /* Line 574: `i_mbufcopy(pq->mp, dp, len);` — attacker bytes via one mbuf */
    struct mbuf m;
    char data[4096];
    memset(data, 0x5a, sizeof data);              /* "attacker" pattern */
    if (attacker_bytes) memcpy(data, attacker_bytes, ds_len < (long)sizeof data ? ds_len : (long)sizeof data);
    m.m_next = NULL; m.m_len = len; m.m_data = data;

    i_mbufcopy(&m, dp, len);

    /* count redzone-sentinel corruption outside [buf, buf+edtlen) */
    long oob = 0;
    /* below: redzone-before (any non-RED byte = corrupted by attacker write) */
    for (char *p = buf - REDZONE; p < buf; p++) if ((unsigned char)*p != RED) oob++;
    /* above: redzone-after */
    for (char *p = buf + edtlen; p < buf + edtlen + REDZONE; p++) if ((unsigned char)*p != RED) oob++;
    return oob;
}

int main(int argc, char **argv) {
    int fixed = (argc > 1 && !strcmp(argv[1], "fixed"));
    long edtlen = 512;                 /* csio->dxfer_len for a 512B SCSI read */
    long base_size = edtlen + 2*REDZONE;  /* REDZONE bytes on each side */
    char *base = (char *)malloc(base_size);
    if (!base) { perror("malloc"); return 2; }

    /* sub-cases: (label, attacker_bo, ds_len, attacker_bytes_or_NULL) */
    struct { const char *name; long bo; long ds; const char *bytes; } cases[] = {
        /* Case A: legal, in-bounds — both unfixed and fixed should be silent */
        {"in_bounds_bo=0_ds=512",      0,    512, NULL},
        /* Case B: positive OOB — bo exactly at end of data_ptr; classic heap overrun into next slab object */
        {"OOB_pos_bo=0x200_ds=0x100",  0x200, 0x100, NULL},
        /* Case C: large positive OOB — write far past allocation */
        {"OOB_pos_bo=0x300_ds=0x80",   0x300, 0x80, NULL},
        /* Case D: negative offset (high-bit bo) — write BELOW data_ptr (sign-extended int offset) */
        {"OOB_neg_bo=0xFFFFFC00_ds=16",0xFFFFFC00UL, 16, NULL},
        /* Case E: OOB with crafted bytes (slab-groom / function-ptr pattern) */
        {"OOB_pos_bo=0x200_ds=16_pattern",
                                         0x200, 16,
            "\x10\x20\x30\x40\x50\x60\x70\x80\x11\x21\x31\x41\x51\x61\x71\x81"},
    };
    int ncases = (int)(sizeof(cases)/sizeof(cases[0]));

    int any_oob = 0;
    long total_oob_bytes = 0;
    for (int i = 0; i < ncases; i++) {
        /* reset: redzone both sides, clear interior */
        memset(base, RED, base_size);
        memset(base + REDZONE, 0, edtlen);
        char *buf = base + REDZONE;       /* == csio->data_ptr */

        long oob = run_decap(buf, edtlen, cases[i].bo, cases[i].ds,
                             cases[i].bytes, fixed);
        const char *verdict;
        if (oob == -1)      verdict = "skip(edtlen<ds_len)";
        else if (oob == -2) verdict = "REJECTED by fix";
        else if (oob == 0)  verdict = "in-bounds (no OOB)";
        else                verdict = "OOB WRITE";
        printf("[%s] mode=%s  %-30s bo=0x%lx ds_len=%ld  -> %s",
               cases[i].name, fixed ? "FIXED" : "UNFIXED",
               cases[i].name, (long)(u_int)cases[i].bo, cases[i].ds, verdict);
        if (oob > 0) {
            printf("  oob_bytes=%ld", oob);
            any_oob = 1;
            total_oob_bytes += oob;
        }
        printf("\n");
    }

    free(base);
    printf("\nSUMMARY mode=%s  total_oob_bytes=%ld  exit=%d\n",
           fixed ? "FIXED" : "UNFIXED", total_oob_bytes, any_oob ? 1 : 0);
    return any_oob ? 1 : 0;
}
