โฌข DragonFlyBSD Kernel Audit
1085 / harness_fixed.c
โ† back to finding โ†“ download raw
/*
 * DF-1085 โ€” crom_parse_text() FIXED variant (crc_len < 2 guard added)
 * File: sys/bus/firewire/fwcrom.c:215
 *
 * This harness compiles the VERBATIM crom_init_context() / crom_get() /
 * crom_parse_text() from fwcrom.c (lines 62-94, 96-103, 188-225) together
 * with the exact structures from iec13213.h, and feeds them a crafted
 * IEEE 1212 Configuration ROM whose text leaf has crc_len == 0.
 *
 * The Configuration ROM is the data structure an EXTERNAL FireWire device
 * presents on the bus.  The kernel parses it when attaching an SBP-2
 * (SCSI-over-FireWire) target โ€” see sys/dev/disk/sbp/sbp.c:602-621
 * (sbp_probe_lun) which calls:
 *
 *     crom_parse_text(cc, sdev->vendor,  sizeof(sdev->vendor));   // sbp.c:606
 *     crom_parse_text(cc, sdev->product, sizeof(sdev->product));  // sbp.c:621
 *
 * A malicious external device can therefore present a ROM whose text leaf
 * header has crc_len == 0 (or 1) to trigger this path.  No FireWire
 * controller exists in the QEMU guest, so we feed the parser directly
 * (the same CROM bytes the kernel would receive).
 *
 * THE BUG (fwcrom.c:215):
 *      qlen = textleaf->crc_len - 2;     // crc_len is u_int32_t:16 (BIT16x2)
 *
 * `textleaf->crc_len` is a 16-bit unsigned field (BIT16x2 macro,
 * firewire.h:122).  When the device sets it to 0 or 1, the subtraction
 * underflows in signed int arithmetic: qlen becomes -2 or -1.
 *
 * The downstream checks then MISBEHAVE because they compare a *signed*
 * `len` (positive) against a NEGATIVE `qlen * 4`:
 *
 *      if (len < qlen * 4)        // 32 < -8  => FALSE, qlen stays negative
 *          qlen = len/4;
 *      for (i = 0; i < qlen; i++) // 0 < -2   => FALSE, loop skipped
 *          *bp++ = ntohl(textleaf->text[i]);
 *      if (len <= qlen * 4)       // 32 <= -8 => FALSE
 *          buf[len - 1] = 0;
 *      else
 *          buf[qlen * 4] = 0;     // *** buf[-8] = 0  (or buf[-4] for crc_len=1)
 *                                  //     OUT-OF-BOUNDS WRITE BEFORE buf ***
 *
 * The single NUL byte is written 4 or 8 bytes BEFORE the caller's buffer.
 * In the SBP-2 caller, buf == sdev->vendor (sbp.c:187); vendor[-8] aliases
 * the high byte of `free_ocbs.tqh_last` (the STAILQ tail pointer of the
 * free-OCB queue, sbp.c:186) โ€” corrupting a kernel heap pointer that is
 * later dereferenced on the next STAILQ_INSERT_TAIL / STAILQ_REMOVE.
 *
 * NOTE on finding summary wording: the finding's prose says "bcopy ->
 * massive heap corruption".  The actual primitive (verified here, line by
 * line against fwcrom.c) is a SINGLE-byte NUL write at buf[-8] (crc_len=0)
 * or buf[-4] (crc_len=1) โ€” the for-loop body is *skipped* because 0 < -2
 * is false.  The underflow is real and exploitable; the "massive" wording
 * overstates the per-call effect.
 *
 * BUILD:  cc -O0 -g -o harness harness.c
 *   (add -fsanitize=address for stack/heap underflow trapping)
 * RUN:    ./harness
 * EXPECT (fixed logic): prints "BUG CONFIRMED" and exits 1 (canary byte zeroed)
 * EXPECT (fixed logic):  prints "canary intact" and exits 0
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>      /* ntohl */

typedef uint32_t u_int32_t;
typedef uint16_t u_int16_t;
typedef uint8_t  u_int8_t;
typedef unsigned long vm_offset_t;

/* ====================================================================
 * From sys/bus/firewire/firewire.h + iec13213.h โ€” copied verbatim
 * (little-endian, x86_64)
 * ==================================================================== */

/* firewire.h:122 โ€” BIT16x2 on little-endian:  u_int32_t y:16, x:16 */
#define BIT16x2(x,y) u_int32_t y:16, x:16

#define CSRTYPE_SHIFT   6
#define CSRTYPE_MASK    (3 << CSRTYPE_SHIFT)
#define CSRTYPE_L       (2 << CSRTYPE_SHIFT)   /* 0x80 โ€” Leaf type          */
#define CSRKEY_DESC     0x01                   /* Descriptor                */
#define CROM_TEXTLEAF   (CSRTYPE_L | CSRKEY_DESC)   /* 0x81 Text leaf       */

struct csrreg {            /* iec13213.h:124 (LE branch) */
    u_int32_t val:24, key:8;
};
struct csrhdr {            /* iec13213.h:133 (LE branch) */
    u_int32_t crc:16, crc_len:8, info_len:8;
};
struct csrdirectory {      /* iec13213.h:144 */
    BIT16x2(crc_len, crc); /*   low16=crc, high16=crc_len */
    struct csrreg entry[0];
};
struct csrtext {           /* iec13213.h:148 */
    BIT16x2(crc_len, crc); /*   low16=crc, high16=crc_len */
    u_int32_t spec_id:24, spec_type:8;   /* LE branch */
    u_int32_t lang_id;
    u_int32_t text[0];
};

#define CROM_MAX_DEPTH  10
struct crom_ptr {          /* iec13213.h:201 */
    struct csrdirectory *dir;
    int index;
};
struct crom_context {      /* iec13213.h:206 */
    int depth;
    struct crom_ptr stack[CROM_MAX_DEPTH];
};

#define MAX_ROM (1024 - sizeof(u_int32_t) * 5)
#define CROM_END(cc) ((vm_offset_t)(cc)->stack[0].dir + MAX_ROM - 1)

/* ====================================================================
 * From sys/bus/firewire/fwcrom.c:62-225 โ€” copied VERBATIM.
 * The only change is kprintf -> printf for userspace.  NOTHING in the
 * logic is altered; this is the exact kernel code.
 * ==================================================================== */

void
crom_init_context(struct crom_context *cc, u_int32_t *p)   /* fwcrom.c:62 */
{
    struct csrhdr *hdr;

    hdr = (struct csrhdr *)p;
    if (hdr->info_len == 0) {
        printf("crom_init_context: WARNING, info_len is 0\n");
        cc->depth = -1;
        return;
    }
    if (hdr->info_len == 1) {
        /* minimum ROM */
        cc->depth = -1;
    }
    p += 1 + hdr->info_len;

    /* check size of root directory */
    if (((struct csrdirectory *)p)->crc_len == 0) {
        cc->depth = -1;
        return;
    }
    cc->depth = 0;
    cc->stack[0].dir = (struct csrdirectory *)p;
    cc->stack[0].index = 0;
}

struct csrreg *
crom_get(struct crom_context *cc)                           /* fwcrom.c:96 */
{
    struct crom_ptr *ptr;

    ptr = &cc->stack[cc->depth];
    return (&ptr->dir->entry[ptr->index]);
}

void
crom_parse_text(struct crom_context *cc, char *buf, int len)  /* fwcrom.c:188 */
{
    struct csrreg *reg;
    struct csrtext *textleaf;
    u_int32_t *bp;
    int i, qlen;
    static const char *nullstr = "(null)";

    if (cc->depth < 0)
        return;

    reg = crom_get(cc);
    if (reg->key != CROM_TEXTLEAF ||
            (vm_offset_t)(reg + reg->val) > CROM_END(cc)) {
        strncpy(buf, nullstr, len);
        return;
    }
    textleaf = (struct csrtext *)(reg + reg->val);

    if ((vm_offset_t)textleaf + textleaf->crc_len > CROM_END(cc)) {
        strncpy(buf, nullstr, len);
        return;
    }

    /* XXX should check spec and type */

    bp = (u_int32_t *)&buf[0];
    /* DF-1085 FIX: reject crc_len < 2 so (crc_len - 2) cannot underflow. */
    if (textleaf->crc_len < 2) {
        strncpy(buf, nullstr, len);
        return;
    }
    qlen = textleaf->crc_len - 2;                          /* LINE 215 โ€” FIXED */
    if (len < qlen * 4)                                    /* LINE 216 */
        qlen = len/4;
    for (i = 0; i < qlen; i ++)                            /* LINE 218 */
        *bp++ = ntohl(textleaf->text[i]);
    /* make sure to terminate the string */
    if (len <= qlen * 4)                                   /* LINE 221 */
        buf[len - 1] = 0;
    else
        buf[qlen * 4] = 0;                                 /* LINE 224 โ€” UNDERFLOW WRITE */
}

/* ====================================================================
 * Harness
 * ==================================================================== */

/*
 * Probe layout: 16 sentinel bytes, then buf[32], then 16 trailing bytes.
 * buf[-8] aliases pre[8]   (the byte the bug zeroes when crc_len == 0)
 * buf[-4] aliases pre[12]  (the byte the bug zeroes when crc_len == 1)
 */
#define BUF_LEN 32
#define PAD     16

struct probe {
    unsigned char pre[PAD];
    char buf[BUF_LEN];
    unsigned char post[PAD];
};

static int run_case(u_int16_t leaf_crc_len, const char *label)
{
    /*
     * Crafted Configuration ROM (little-endian u_int32_t words):
     *   word[0]  csrhdr:        info_len=1            -> 0x01000000
     *   word[1]  bus_info:      1 word, zero           (info_len==1)
     *   word[2]  root dir hdr:  crc_len=1 (1 entry)   -> 0x00010000
     *   word[3]  root entry[0]: key=CROM_TEXTLEAF(0x81), val=1
     *            -> reg + reg->val = &entry[0] + 1*sizeof(csrreg)
     *                           = &word[4]  (the text leaf header)
     *   word[4]  text leaf hdr: crc_len=<leaf_crc_len>  *** attacker-controlled ***
     *   word[5]  spec_id/spec_type: 0
     *   word[6]  lang_id:           0
     *   (no text words when crc_len <= 2)
     */
    u_int32_t rom[32];
    memset(rom, 0, sizeof(rom));
    rom[0] = (1u << 24);                                   /* csrhdr info_len=1 */
    rom[1] = 0;                                            /* bus_info (1 word) */
    rom[2] = (1u << 16);                                   /* root dir crc_len=1 */
    rom[3] = ((u_int32_t)CROM_TEXTLEAF << 24) | 1u;        /* leaf entry, val=1 */
    /* BIT16x2 LE layout: low16=crc, high16=crc_len */
    rom[4] = ((u_int32_t)leaf_crc_len << 16);              /* TEXT LEAF crc_len */
    rom[5] = 0;                                            /* spec */
    rom[6] = 0;                                            /* lang */

    struct probe p;
    memset(&p, 0xA5, sizeof(p));     /* fill everything with 0xA5 sentinels */
    memset(p.buf, 0xBB, BUF_LEN);    /* buf distinct sentinel */

    struct crom_context cc;
    crom_init_context(&cc, rom);
    if (cc.depth < 0) {
        printf("[%s] crom_init_context returned depth=-1, aborting case\n", label);
        return -1;
    }
    printf("[%s] root dir entry[0]: key=0x%02x val=0x%06x; leaf crc_len=%u\n",
           label,
           crom_get(&cc)->key, crom_get(&cc)->val,
           leaf_crc_len);

    /* The call site under test.  Mirrors sbp.c:606. */
    crom_parse_text(&cc, p.buf, BUF_LEN);

    /* The bug writes a single NUL byte to buf[qlen*4].  For crc_len=0
     * qlen=-2, so buf[-8] == pre[8] is zeroed.
     * For crc_len=1 qlen=-1, so buf[-4] == pre[12] is zeroed. */
    int expected_idx = PAD + ((int)leaf_crc_len - 2) * 4;  /* index into p.pre+buf */
    /* Translate to offset within `struct probe`: */
    int probe_idx_pre8  = 8;     /* buf[-8] when crc_len=0 */
    int probe_idx_pre12 = 12;    /* buf[-4] when crc_len=1 */

    /* Snapshot what got hit */
    int hit = 0;
    if (leaf_crc_len == 0 && p.pre[8] == 0x00)  hit = 8;
    if (leaf_crc_len == 1 && p.pre[12] == 0x00) hit = 12;

    /* Dump the 16-byte preamble around buf */
    printf("[%s] pre  bytes: ", label);
    for (int i = 0; i < PAD; i++) printf("%02x ", p.pre[i]);
    printf("\n[%s] buf [0..3]: %02x %02x %02x %02x (sentinel was BB BB BB BB)\n",
           label,
           (unsigned char)p.buf[0], (unsigned char)p.buf[1],
           (unsigned char)p.buf[2], (unsigned char)p.buf[3]);

    if (hit) {
        printf("[%s] >>> BUG CONFIRMED: pre[%d] (== buf[%d]) was zeroed by "
               "buf[qlen*4]=0 with qlen=%d\n",
               label, hit, hit - PAD, (int)leaf_crc_len - 2);
        printf("[%s] >>> In the SBP-2 caller this byte aliases the high byte "
               "of sdev->free_ocbs.tqh_last (sbp.c:186), corrupting a\n"
               "[%s] >>> kernel heap pointer later dereferenced by "
               "STAILQ_INSERT_TAIL/REMOVE on the free-OCB queue.\n",
               label, label);
        return 1;
    }
    printf("[%s] canary intact: no underflow write (crc_len guard fired).\n",
           label);
    return 0;
}

int main(void)
{
    printf("=== DF-1085 harness: crom_parse_text write-underflow "
           "(textleaf->crc_len < 2) ===\n");
    printf("fwcrom.c:215  qlen = textleaf->crc_len - 2;   "
           "(crc_len is u_int32_t:16, BIT16x2)\n");
    printf("fwcrom.c:224  buf[qlen * 4] = 0;              "
           "(qlen=-2 -> buf[-8]; qlen=-1 -> buf[-4])\n\n");

    int r0 = run_case(0, "crc_len=0");
    printf("\n");
    int r1 = run_case(1, "crc_len=1");
    printf("\n");
    /* Sanity: crc_len=2 is the minimum legal value (no text), should NOT trigger */
    int r2 = run_case(2, "crc_len=2 (legal minimum)");

    printf("\n=== summary ===\n");
    printf("crc_len=0 -> %s\n", r0 == 1 ? "BUG (underflow write)" :
                                 (r0 == 0 ? "no bug" : "n/a"));
    printf("crc_len=1 -> %s\n", r1 == 1 ? "BUG (underflow write)" :
                                 (r1 == 0 ? "no bug" : "n/a"));
    printf("crc_len=2 -> %s (control: must be 'no bug')\n",
                                 r2 == 0 ? "no bug" : "BUG??");

    if (r0 == 1 || r1 == 1) {
        printf("\n>>> OVERALL: BUG CONFIRMED โ€” crom_parse_text writes out "
               "of bounds when crc_len < 2.\n");
        return 1;
    }
    printf("\n>>> OVERALL: canary intact โ€” underflow is guarded.\n");
    return 0;
}