DragonFlyBSD Kernel Audit
DF-1125 / harness.c
← back to finding ↓ download raw
/*
 * DF-1125 harness: isp.c 24XX FCP response/sense OOB read (source-trace proof)
 *
 * Replicates the EXACT kernel data-flow of isp_handle_intr_response() for the
 * 24XX (QLogic FC) RQSTYPE_RESPONSE path:
 *
 *   sys/dev/disk/isp/isp.c:5042   uint8_t qe[QENTRY_LEN];   // QENTRY_LEN = 64
 *   sys/dev/disk/isp/isp.c:5225   rlen = sp->req_response_len;          // unvalidated, FW-controlled
 *   sys/dev/disk/isp/isp.c:5238   snsp = sp->req_rsp_sense;             // &qe[36]
 *   sys/dev/disk/isp/isp.c:5239   snsp += rlen;                         // 36 + rlen  -> OOB if rlen>=28
 *   sys/dev/disk/isp/isp.c:5240   slen = sp->req_sense_len;             // unvalidated, FW-controlled
 *   sys/dev/disk/isp/isp.c:5307   if (snsp && slen) XS_SAVE_SENSE(xs, snsp, slen);
 *        -> isp_freebsd.h:483     memcpy(&xs->sense_data, snsp, imin(XS_SNSLEN, slen));
 *
 * isp24xx_statusreq_t (ispmbox.h:654-668) is exactly 64 bytes; req_rsp_sense[28]
 * is the final field at offset 36. A malicious remote FC target that sends an
 * FCP_RSP IU with FCP_RSP_LEN (req_response_len) >= 28 drives `snsp` past the
 * 64-byte stack buffer; XS_SAVE_SENSE then memcpy()s up to 32 bytes from the
 * wild pointer into xs->sense_data -> kernel stack info leak (small rlen) or
 * panic from an unmapped page (large rlen).
 *
 * This is a userspace replica of that arithmetic, proving the OOB read
 * concretely. The kernel path itself requires a QLogic 24XX FC HBA + a remote
 * FC target, neither of which is present on the audit QEMU guest (virtio only).
 */

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

#define QENTRY_LEN 64
#define REQ_RSP_SENSE_LEN 28
#define REQ_RSP_SENSE_OFF 36            /* 64 - 28 */
#define XS_SNSLEN 32                    /* SSD_SENSELEN / sizeof sense_data */

typedef struct {
    uint8_t  rqs_entry_type;
    uint8_t  rqs_entry_count;
    uint8_t  rqs_seqno;
    uint8_t  rqs_flags;
} isphdr_t;

typedef struct {
    isphdr_t req_header;
    uint32_t req_handle;
    uint16_t req_completion_status;
    uint16_t req_oxid;
    uint32_t req_resid;
    uint16_t req_reserved0;
    uint16_t req_state_flags;
    uint16_t req_reserved1;
    uint16_t req_scsi_status;
    uint32_t req_fcp_residual;
    uint32_t req_sense_len;
    uint32_t req_response_len;
    uint8_t  req_rsp_sense[REQ_RSP_SENSE_LEN];
} isp24xx_statusreq_t;

/* sizeof verification: must be exactly QENTRY_LEN, matching the kernel layout. */
struct { char z[QENTRY_LEN]; } qe_sizeof_check;
struct { char z[sizeof(isp24xx_statusreq_t)]; } stat_sizeof_check;
/* compile-time assertion that the struct fills the queue entry exactly */
typedef int assert_req_rsp_sense_is_last_field[
    REQ_RSP_SENSE_OFF + REQ_RSP_SENSE_LEN == QENTRY_LEN ? 1 : -1];

static size_t imin(size_t a, size_t b) { return a < b ? a : b; }

int main(void)
{
    printf("sizeof(isp24xx_statusreq_t) = %zu (QENTRY_LEN=%d)\n",
           sizeof(isp24xx_statusreq_t), QENTRY_LEN);
    printf("req_rsp_sense at offset %d, length %d\n",
           REQ_RSP_SENSE_OFF, REQ_RSP_SENSE_LEN);

    int oob_cases = 0;
    /* Try several firmware-controlled rlen values a malicious FC target could
     * place in the FCP_RSP IU. Each one the kernel accepts unvalidated. */
    uint32_t rlens[] = { 0, 4, 27, 28, 32, 64, 0x1000 };
    for (size_t c = 0; c < sizeof(rlens)/sizeof(rlens[0]); c++) {
        uint32_t rlen = rlens[c];
        /* qe lives on the kernel stack (isp.c:5042). Model it as a heap block
         * with a guard region immediately after, so we can detect the OOB. */
        uint8_t *qe = calloc(1, QENTRY_LEN);
        isp24xx_statusreq_t *sp = (isp24xx_statusreq_t *)qe;
        sp->req_response_len = rlen;
        sp->req_sense_len    = XS_SNSLEN;   /* attacker asks for full 32-byte sense */

        /* ---- exact kernel arithmetic (isp.c:5238-5240) ---- */
        uint8_t *snsp = sp->req_rsp_sense;  /* &qe[36] */
        snsp += rlen;                       /* 36 + rlen */
        uint32_t slen = sp->req_sense_len;

        size_t read_off = (size_t)(snsp - qe);
        size_t copy_n   = imin(XS_SNSLEN, slen);
        size_t read_end = read_off + copy_n;

        int oob = (read_end > QENTRY_LEN);
        printf("rlen=%-6u -> snsp offset=%-5zu  memcpy=%-2zu bytes  read range=[%zu,%zu) %s\n",
               rlen, read_off, copy_n, read_off, read_end,
               oob ? ">>> OOB READ PAST qe[64]" : "(in bounds)");
        if (oob)
            oob_cases++;

        /* For the in-bounds-leak boundary case, actually perform the copy into
         * a sense buffer to show that attacker-controlled rlen>=28 reads fully
         * past the stack buffer (the bytes would be adjacent kernel stack). */
        if (rlen == 28) {
            uint8_t sense[XS_SNSLEN] = {0};
            /* emulate memcpy(&xs->sense_data, snsp, imin(XS_SNSLEN,slen)).
             * snsp here points at qe+64; copying 32 bytes is entirely OOB. We
             * guard the source by mapping a canary right after qe. */
            uint8_t *region = calloc(1, QENTRY_LEN + XS_SNSLEN);
            memset(region + QENTRY_LEN, 0xA1, XS_SNSLEN);   /* "adjacent stack" */
            uint8_t *snsp_model = region + REQ_RSP_SENSE_OFF + rlen; /* == region+64 */
            memcpy(sense, snsp_model, imin(XS_SNSLEN, slen));
            printf("   leak demo: sense bytes copied from past qe[64] = ");
            for (size_t i = 0; i < XS_SNSLEN; i++) printf("%02x", sense[i]);
            printf("  (all 0xA1 == adjacent-stack marker)\n");
            free(region);
        }
        free(qe);
    }

    printf("\nresult: %d/%zu firmware-controlled rlen values read past the 64-byte stack entry\n",
           oob_cases, sizeof(rlens)/sizeof(rlens[0]));
    if (oob_cases > 0) {
        printf("DF-1125: CONFIRMED OOB read primitive (kernel stack info leak / panic)\n");
    } else {
        printf("DF-1125: NOT reproduced\n");
    }

    /* ---- WITH FIX (fix.diff clamps): rlen<=28, slen<=28-rlen -> 0 OOB ---- */
    printf("\n--- WITH FIX (clamp rlen to sizeof(req_rsp_sense), slen to 28-rlen) ---\n");
    int fix_oob = 0;
    for (size_t c = 0; c < sizeof(rlens)/sizeof(rlens[0]); c++) {
        uint32_t rlen = rlens[c];
        uint32_t f_rlen = rlen > REQ_RSP_SENSE_LEN ? REQ_RSP_SENSE_LEN : rlen;
        uint32_t f_slen = XS_SNSLEN;
        f_slen = f_slen > (REQ_RSP_SENSE_LEN - f_rlen) ? (REQ_RSP_SENSE_LEN - f_rlen) : f_slen;
        size_t read_off = REQ_RSP_SENSE_OFF + f_rlen;
        size_t read_end = read_off + imin(XS_SNSLEN, f_slen);
        int oob = (read_end > QENTRY_LEN);
        if (oob) fix_oob++;
        printf("rlen=%-6u -> clamped rlen=%-3u slen=%-3u read range=[%zu,%zu) %s\n",
               rlen, f_rlen, f_slen, read_off, read_end,
               oob ? ">>> STILL OOB" : "(in bounds)");
    }
    printf("FIX result: %d OOB cases remain (expect 0)\n", fix_oob);
    printf("DF-1125 FIX: %s\n", fix_oob == 0 ? "VALIDATED - clamps eliminate all OOB reads" : "INCOMPLETE");
    return (oob_cases > 0) ? 0 : 1;
}