DragonFlyBSD Kernel Audit
DF-0861 / harness.c
← back to finding ↓ download raw
/*
 * DF-0861 — Deterministic harness for the unbounded code-page inner-loop
 *           heap OOB WRITE in hpfs_cpinit()
 *           (sys/vfs/hpfs/hpfs_subr.c:274,276,282,292,297 + bcopy :230).
 *
 * This is a FAITHFUL userspace transcription of the buggy mount-time loop
 * against the EXACT on-disk struct layouts from sys/vfs/hpfs/hpfs.h:
 *
 *   struct cpdblk  = 136 bytes (b_country u16, b_cpid u16, b_dbcscnt u16,
 *                               b_upcase[0x80]=128, b_dbcsrange u16)
 *   struct cpiblk  = 16  bytes
 *   struct cpisec  = 512 bytes (s_magic u32, s_cpicnt u32, s_cpifirst u32,
 *                                s_next lsn_t, s_cpi[0x1F]=31*16=496)
 *   struct cpdsec  = 434 bytes (d_magic u32, d_cpcnt u16, d_cpfirst u16,
 *                                d_checksum[3], d_offset[3], d_cpdblk[3])
 *
 * Buggy kernel logic transcribed verbatim:
 *
 *   cpicnt = sp_cpinum;                                          // :274
 *   hpm_cpdblk = kmalloc(cpicnt * sizeof(struct cpdblk));        // :276
 *   cpdbp = hpm_cpdblk;
 *   while (cpicnt > 0) {                                         // :282
 *       cpisp = (struct cpisec *)bread(lsn)->b_data;
 *       cpibp = cpisp->s_cpi;
 *       for (i=0; i < cpisp->s_cpicnt;                           // :292
 *                i++, cpicnt--, cpdbp++, cpibp++) {
 *           hpfs_cpload(cpibp, cpdbp);                           // :297
 *           // hpfs_cpload: bcopy(cpdsp->d_cpdblk[k], cpdbp, 136) // :230
 *       }
 *   }
 *
 * The OUTER while() decrements `cpicnt` (total), but the INNER for() is
 * bounded by `cpisp->s_cpicnt` (per-sector) and runs to completion even
 * after `cpicnt` has gone <= 0.  `cpdbp` is advanced on every inner iter,
 * so a sector advertising s_cpicnt > sp_cpinum writes 136 B * (s_cpicnt -
 * sp_cpinum) past the kmalloc'd hpm_cpdblk array.
 *
 * Poisoned allocator: hpm_cpdblk is placed at the END of the first mmap'd
 * page; the next page(s) are poisoned with 0xAA.  Any byte written past the
 * array lands in the poison region, modelling the slab neighbourhood the
 * attacker does not own.
 *
 * Compile:  cc -O2 -o harness harness.c
 * Run:      ./harness
 *
 * Expected (BUG PRESENT, sp_cpinum=1, s_cpicnt=0x1F):
 *   inner loop ran 31 iters ; in-bounds writes=1 ; OOB writes=30
 *   OOB write = 30 * 136 = 4080 bytes past the 136-byte hpm_cpdblk array
 *   poison HIT
 *   DF_0861_BUG_CONFIRMED=1  DF_0861_BUG_OOB_WRITE_BYTES=4080
 *
 * Expected (FIXED): inner loop clamped so cpicnt never goes < 0; OR sp_cpinum
 * rejected as EINVAL -> 0 OOB writes, no poison hit.
 */

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

/* ---- Exact struct layouts from sys/vfs/hpfs/hpfs.h --------------------- */

typedef struct cpdblk {
    uint16_t b_country;
    uint16_t b_cpid;
    uint16_t b_dbcscnt;
    uint8_t  b_upcase[0x80];
    uint16_t b_dbcsrange;
} cpdblk_t;                      /* sizeof = 136 */

typedef struct cpiblk {
    uint16_t b_country;
    uint16_t b_cpid;
    uint32_t b_checksum;
    uint32_t b_cpdsec;           /* lsn_t */
    uint16_t b_vcpid;
    uint16_t b_dbcscnt;
} cpiblk_t;                      /* sizeof = 16 */

typedef struct cpisec {
    uint32_t s_magic;
    uint32_t s_cpicnt;           /* <-- attacker-controlled, UNVALIDATED */
    uint32_t s_cpifirst;
    uint32_t s_next;
    cpiblk_t s_cpi[0x1F];        /* 31 entries */
} cpisec_t;                      /* sizeof = 512 */

typedef struct cpdsec {
    uint32_t d_magic;
    uint16_t d_cpcnt;
    uint16_t d_cpfirst;
    uint32_t d_checksum[3];
    uint16_t d_offset[3];
    cpdblk_t d_cpdblk[3];
} cpdsec_t;

#define CPDB_SIZE   (sizeof(cpdblk_t))    /* 136 */
#define CPIB_SIZE   (sizeof(cpiblk_t))    /* 16  */
#define CPIS_MAX    0x1F                  /* 31 cpiblk per sector */

/* ---- Poisoned allocator ----------------------------------------------- *
 * Place the (cpicnt*136)-byte hpm_cpdblk array at the very end of the first
 * page; the following pages are poisoned 0xAA.  Any OOB write lands in the
 * poison region, exactly as it would land in a slab neighbour in the kernel. */
static char *
poison_alloc_array(size_t need_bytes, size_t pagesz, uint8_t poison)
{
    /* need at least 2 pages so there is always a poison guard ahead */
    size_t npg = 2;
    while ((npg - 1) * pagesz < need_bytes) npg++;
    size_t total = npg * pagesz;
    char *m = mmap(NULL, total, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANON, -1, 0);
    if (m == MAP_FAILED) { perror("mmap"); exit(2); }
    /* poison everything except the last `need_bytes` of the first page-block */
    char *array = m + (npg - 1) * pagesz - need_bytes;
    /* poison from m up to array (within first page) and all later pages */
    memset(m, poison, (size_t)(array - m));
    memset(array + need_bytes, poison, total - (size_t)(array + need_bytes - m));
    return array;
}

/* ---- Build a forged cpisec sector ------------------------------------- */
static void
forge_cpisec(cpisec_t *cs, uint32_t scpicnt, uint32_t cpdsec_lsn)
{
    memset(cs, 0, sizeof(*cs));
    cs->s_magic = 0x494521F7;
    cs->s_cpicnt = scpicnt;
    cs->s_cpifirst = 0;
    cs->s_next = 0;
    uint32_t n = scpicnt > CPIS_MAX ? CPIS_MAX : scpicnt;
    for (uint32_t k = 0; k < n; k++) {
        cs->s_cpi[k].b_country = 1;
        cs->s_cpi[k].b_cpid    = 1;        /* matches cpdsec */
        cs->s_cpi[k].b_cpdsec  = cpdsec_lsn;
        cs->s_cpi[k].b_vcpid   = 1;
    }
}

/* ---- Build a forged cpdsec sector with attacker content --------------- */
static void
forge_cpdsec(cpdsec_t *ds)
{
    memset(ds, 0, sizeof(*ds));
    ds->d_magic   = 0x894521F7;
    ds->d_cpcnt   = 1;
    ds->d_cpfirst = 0;
    ds->d_cpdblk[0].b_country = 1;
    ds->d_cpdblk[0].b_cpid    = 1;          /* matches cpiblk */
    /* attacker marker in b_upcase so OOB bytes are unmistakable */
    for (int j = 0; j < 0x80; j++)
        ds->d_cpdblk[0].b_upcase[j] = (uint8_t)(0x41 + j);
}

/* ---- Transcription of hpfs_cpload bcopy (subr.c:228-236) ------------- *
 * Walks d_cpfirst..d_cpcnt looking for matching b_cpid; on match, bcopies
 * 136 bytes from d_cpdblk[k] into cpdbp.  Returns 1 if a bcopy happened. */
static int
cpload_bcopy(const cpdsec_t *ds, const cpiblk_t *cb, cpdblk_t *cpdbp,
             char *buf_end, int *poison_hit)
{
    for (uint16_t i = ds->d_cpfirst; i < ds->d_cpcnt; i++) {
        if (ds->d_cpdblk[i].b_cpid == cb->b_cpid) {
            char *dst = (char *)cpdbp;
            for (size_t b = 0; b < CPDB_SIZE; b++) {
                if (dst + b >= buf_end && dst[b] == 0xAA) *poison_hit = 1;
                dst[b] = ((char *)&ds->d_cpdblk[i])[b];
            }
            return 1;
        }
    }
    return 0;  /* no match -> ENOENT (kernel returns, mount aborts) */
}

/* ---- Transcription of the buggy hpfs_cpinit loop (subr.c:282-305) ---- *
 * FIXED=0 reproduces the kernel bug exactly; FIXED=1 clamps the inner loop
 * so it never advances cpdbp past the array (the proposed fix). */
static void
run_cpinit(char *hpm_cpdblk, int cpicnt, const cpisec_t *cs,
           const cpdsec_t *ds, int fixed,
           uint32_t *inbounds_out, uint32_t *oob_iters_out,
           size_t *oob_bytes_out, int *poison_hit, char *buf_end)
{
    cpdblk_t *cpdbp = (cpdblk_t *)hpm_cpdblk;
    cpiblk_t *cpibp = cs->s_cpi;
    char *array_end = hpm_cpdblk + (size_t)cpicnt * CPDB_SIZE;
    uint32_t inb = 0, oob = 0;
    int i;
    int local_cpicnt = cpicnt;

    for (i = 0; i < (int)cs->s_cpicnt; i++, local_cpicnt--, cpdbp++, cpibp++) {
        /* FIXED: stop once we've exhausted the legitimately-allocated slots */
        if (fixed && cpdbp >= (cpdblk_t *)array_end) break;

        int wrote = cpload_bcopy(ds, cpibp, cpdbp, buf_end, poison_hit);
        if (!wrote) {
            /* kernel returns ENOENT and aborts mount; emulate by stopping */
            break;
        }
        if ((char *)cpdbp >= array_end)
            oob++;
        else
            inb++;
    }
    (void)local_cpicnt;
    *inbounds_out = inb;
    *oob_iters_out = oob;
    *oob_bytes_out = (size_t)oob * CPDB_SIZE;
}

int main(void)
{
    size_t pagesz = sysconf(_SC_PAGESIZE);

    /* ---- struct-size sanity (must match the kernel layout exactly) ---- */
    printf("=== DF-0861 deterministic OOB-WRITE proof ===\n");
    printf("struct sizes: cpdblk=%zu cpiblk=%zu cpisec=%zu (CPIS_MAX=%d)\n",
           sizeof(cpdblk_t), sizeof(cpiblk_t), sizeof(cpisec_t), CPIS_MAX);
    if (sizeof(cpdblk_t) != 136 || sizeof(cpiblk_t) != 16 ||
        sizeof(cpisec_t) != 512) {
        printf("ERROR: struct size mismatch with hpfs.h layout\n");
        return 2;
    }
    printf("\n");

    /* Forged values (same as the crafted image): sp_cpinum=1, s_cpicnt=0x1F */
    uint32_t sp_cpinum = 1;
    uint32_t s_cpicnt  = 0x1F;   /* 31 */

    cpisec_t cs; forge_cpisec(&cs, s_cpicnt, /*cpdsec_lsn=*/0x70);
    cpdsec_t ds; forge_cpdsec(&ds);

    int cpicnt = (int)sp_cpinum;
    size_t alloc_bytes = (size_t)cpicnt * CPDB_SIZE;   /* 1*136 = 136 */

    printf("--- BUG MODE (unpatched hpfs_cpinit) ---\n");
    printf("  sp_cpinum=%u -> cpicnt=%d -> kmalloc(hpm_cpdblk)=%zu bytes\n",
           sp_cpinum, cpicnt, alloc_bytes);
    printf("  cpisec.s_cpicnt=%u -> inner for() runs %u times\n",
           s_cpicnt, s_cpicnt);
    char *arr = poison_alloc_array(alloc_bytes, pagesz, 0xAA);
    char *buf_end = arr + alloc_bytes;  /* end of the legitimate array */
    /* but the poison region extends well beyond; give cpload a generous
     * "buffer end" = far past, so the per-byte poison check inside cpload
     * fires on the 0xAA guard rather than just the array end. */
    char *far_end = arr + 16 * pagesz;
    uint32_t inb = 0, oob = 0; size_t oob_bytes = 0; int poison = 0;
    run_cpinit(arr, cpicnt, &cs, &ds, /*fixed=*/0,
               &inb, &oob, &oob_bytes, &poison, far_end);
    printf("  result: in-bounds writes=%u  OOB writes=%u  OOB bytes=%zu  poison=%s\n",
           inb, oob, oob_bytes, poison ? "HIT" : "no");
    printf("  DF_0861_BUG_OOB_WRITE_BYTES=%zu\n", oob_bytes);
    printf("  DF_0861_BUG_CONFIRMED=%d\n\n", oob ? 1 : 0);

    /* ---- Verify OOB content is attacker-controlled (marker 0x41..) ---- */
    int content_ok = 0;
    if (oob) {
        /* first OOB slot = arr + CPDB_SIZE; check b_upcase[0..3] */
        char *first_oob = arr + CPDB_SIZE;
        if ((uint8_t)first_oob[CPDB_SIZE - 136 + 6] == 0x41) content_ok = 1;
        /* simpler: byte at arr+136+6 (b_upcase[0] of first OOB cpdblk) */
        uint8_t up0 = (uint8_t)arr[CPDB_SIZE + 6];
        uint8_t up1 = (uint8_t)arr[CPDB_SIZE + 7];
        printf("  first-OOB cpdblk b_upcase[0..1] = 0x%02x 0x%02x (marker 0x41 0x42) -> %s\n",
               up0, up1, (up0==0x41 && up1==0x42) ? "ATTACKER-CONTROLLED" : "not-marker");
        if (up0 == 0x41 && up1 == 0x42) content_ok = 1;
    }

    /* ---- FIXED MODE: inner loop clamped to the allocated slots -------- */
    printf("--- FIXED MODE (inner loop bounded by allocated slots) ---\n");
    char *arr2 = poison_alloc_array(alloc_bytes, pagesz, 0xAA);
    uint32_t inb2 = 0, oob2 = 0; size_t oob_bytes2 = 0; int poison2 = 0;
    run_cpinit(arr2, cpicnt, &cs, &ds, /*fixed=*/1,
               &inb2, &oob2, &oob_bytes2, &poison2, far_end);
    printf("  result: in-bounds writes=%u  OOB writes=%u  OOB bytes=%zu  poison=%s\n",
           inb2, oob2, oob_bytes2, poison2 ? "HIT" : "no");
    printf("  DF_0861_FIX_OOB_WRITE_BYTES=%zu\n", oob_bytes2);
    printf("  DF_0861_FIX_REJECTS_OVERFLOW=%d\n\n", oob2 ? 0 : 1);

    printf("=== SUMMARY ===\n");
    printf("DF_0861_BUG_CONFIRMED=%d\n", oob ? 1 : 0);
    printf("DF_0861_BUG_OOB_WRITE_BYTES=%zu\n", oob_bytes);
    printf("DF_0861_BUG_OOB_ATTACKER_CONTROLLED=%d\n", content_ok);
    printf("DF_0861_FIX_REJECTS_OVERFLOW=%d\n", oob2 ? 0 : 1);
    return (oob && oob_bytes2 == 0) ? 0 : 1;
}