โฌข DragonFlyBSD Kernel Audit
DF-0857 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0857 โ€” Deterministic harness for missing ab_busycnt validation
 *           in hpfs_hpbmap() (sys/vfs/hpfs/hpfs_alsubr.c:80, :114).
 *
 * This is a FAITHFUL userspace transcription of the two iteration loops in
 * the DragonFlyBSD kernel's hpfs_hpbmap(), against the EXACT on-disk struct
 * layouts from sys/vfs/hpfs/hpfs.h (alblk_t, alleaf_t, alnode_t, fnode.fn_abd,
 * alsec.as_abd).  The harness proves โ€” deterministically, byte-for-byte โ€”
 * that a forged ab_busycnt of 255 walks the loop past the data area of the
 * container and into a neighbouring heap object.
 *
 * Layout in sys/vfs/hpfs/hpfs.h:
 *   alblk_t  = 8 bytes  (ab_flag, ab_res[3], ab_freecnt, ab_busycnt, ab_freeoff)
 *   alleaf_t = 12 bytes (al_off, al_len, al_lsn)
 *   alnode_t = 8 bytes  (an_nextoff, an_lsn)
 *   struct fnode  has u_int8_t fn_abd[0x60]   = 96-byte data area
 *   struct alsec  has u_int8_t as_abd[0x1E0]  = 480-byte data area
 *
 * In-kernel container:
 *   struct hpfsnode is kmalloc'd (hpfs_vfsops.c:488).  Its embedded struct
 *   fnode (h_fn) carries fn_abd[] inline; overrunning fn_abd reads past the
 *   hpfsnode allocation into whatever sits next to it on the slab heap.
 *
 * Poisoned allocator: we mmap two consecutive pages, fill the SECOND page
 * with 0xAA (poison), then place the in-memory fnode / alsec container at the
 * END of the first page so that any read past the data area falls into the
 * 0xAA page.  This models the slab-neighbour case exactly: the bytes the
 * kernel loop dereferences are bytes the attacker does NOT own.
 *
 * The harness then transcribes both loops (AB_NODES at :80, leaf at :114) and
 * measures how far past the legitimate end of fn_abd / as_abd the loop reads.
 *
 * Compile:  cc -O2 -o harness harness.c
 * Run:      ./harness
 *
 * Expected (BUG PRESENT): prints OOB_READ_BYTES_LEAF=3060 and NODE=2040 for
 *          the fnode container (and 3060/2040 for the alsec container), plus
 *          the poisoned-region probe (0xAA) confirms the loop walked into the
 *          neighbour.
 * Expected (FIXED):       the bounds-check rejects forged busycnt and the
 *          loops never execute past the legitimate end.
 */

#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 --------------------- */

#define AB_NODES    0x80
#define AB_FNPARENT 0x20

typedef struct alblk {
    uint8_t  ab_flag;
    uint8_t  ab_res[3];
    uint8_t  ab_freecnt;
    uint8_t  ab_busycnt;       /* <-- attacker-controlled, UNVALIDATED */
    uint16_t ab_freeoff;
} alblk_t;                     /* sizeof = 8 */

typedef struct alleaf {
    uint32_t al_off;
    uint32_t al_len;
    uint32_t al_lsn;
} alleaf_t;                     /* sizeof = 12 */

typedef struct alnode {
    uint32_t an_nextoff;
    uint32_t an_lsn;
} alnode_t;                     /* sizeof = 8 */

/* fnode alloc-block data area (96 bytes) */
#define FN_ABD_SIZE   0x60
/* alsec alloc-block data area (480 bytes) */
#define AS_ABD_SIZE   0x1E0

/* A struct that mimics just the head of struct fnode the way hpfs_hpbmap
 * sees it: ab pointer + abd data area. */
typedef struct fnode_container {
    alblk_t  fn_ab;
    uint8_t  fn_abd[FN_ABD_SIZE];
} fnode_container_t;

typedef struct alsec_container {
    alblk_t  as_ab;
    uint8_t  as_abd[AS_ABD_SIZE];
} alsec_container_t;

/* Legitimate maximum busycnt per container (data area / element size) */
#define FN_MAX_LEAF   (FN_ABD_SIZE / sizeof(alleaf_t))   /*  8 */
#define FN_MAX_NODE   (FN_ABD_SIZE / sizeof(alnode_t))   /* 12 */
#define AS_MAX_LEAF   (AS_ABD_SIZE / sizeof(alleaf_t))   /* 40 */
#define AS_MAX_NODE   (AS_ABD_SIZE / sizeof(alnode_t))   /* 60 */

/* ---- Poisoned allocator ----------------------------------------------- */
/* Place the container at the END of the first page; the second page is
 * poisoned (0xAA).  Any read past the container's data area lands in the
 * poison page, modelling the slab-neighbour case. */
static void *
poison_alloc(size_t container_size, size_t pagesz)
{
    size_t total = pagesz * 2;
    void *m = mmap(NULL, total, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANON, -1, 0);
    if (m == MAP_FAILED) { perror("mmap"); exit(2); }
    /* poison the second page */
    memset((char *)m + pagesz, 0xAA, pagesz);
    /* place container so its end is exactly at the page boundary */
    char *p = (char *)m + (pagesz - container_size);
    return p;
}

/* ---- Transcription of hpfs_hpbmap() loops (sys/vfs/hpfs/hpfs_alsubr.c) - */
/* Returns the number of bytes the loop read past the END of the data area
 * (abd/abd_size).  0 means no OOB.  Forges busycnt=255, doesn't take any
 * early-exit branch (bn chosen to never match any leaf), so the loop runs to
 * completion โ€” exactly the worst case the bug allows. */
static size_t
run_loop(alblk_t *abp, void *abd, size_t abd_size, uint8_t forged_busycnt,
         uint32_t bn, int *poison_hit)
{
    size_t oob = 0;
    *poison_hit = 0;

    if (abp->ab_flag & AB_NODES) {
        /* alnode loop : hpfs_alsubr.c:80 */
        alnode_t *anp = (alnode_t *)abd;
        char *abd_end = (char *)abd + abd_size;
        for (uint32_t i = 0; i < abp->ab_busycnt; i++, anp++) {
            char *cur = (char *)anp;
            char *elt_end = cur + sizeof(alnode_t);
            if (elt_end > abd_end) {
                size_t over = (size_t)(elt_end - abd_end);
                if (over > oob) oob = over;
                /* sample poison: did we dereference into the 0xAA page? */
                if (((uint8_t *)anp)[0] == 0xAA &&
                    ((uint8_t *)anp)[sizeof(alnode_t)-1] == 0xAA)
                    *poison_hit = 1;
            }
            /* The kernel derefs anp->an_nextoff and anp->an_lsn here
             * (lines 81-82). We emulate the read. */
            (void)anp->an_nextoff;
            (void)anp->an_lsn;
            (void)bn;
        }
    } else {
        /* alleaf loop : hpfs_alsubr.c:114 */
        alleaf_t *alp = (alleaf_t *)abd;
        char *abd_end = (char *)abd + abd_size;
        for (uint32_t i = 0; i < abp->ab_busycnt; i++, alp++) {
            char *cur = (char *)alp;
            char *elt_end = cur + sizeof(alleaf_t);
            if (elt_end > abd_end) {
                size_t over = (size_t)(elt_end - abd_end);
                if (over > oob) oob = over;
                if (((uint8_t *)alp)[0] == 0xAA &&
                    ((uint8_t *)alp)[sizeof(alleaf_t)-1] == 0xAA)
                    *poison_hit = 1;
            }
            /* The kernel derefs alp->al_off, alp->al_len, alp->al_lsn here
             * (lines 115-119, 122). We emulate the read. */
            (void)alp->al_off;
            (void)alp->al_len;
            (void)alp->al_lsn;
            (void)bn;
        }
    }
    return oob;
}

/* The FIXED loops: bounds-check busycnt against container max BEFORE walking.
 * Mirrors the proposed fix.diff.  Returns 0 (no OOB) if accepted, or -1 if
 * rejected (would-be attacker input refused). */
static int
run_loop_fixed(alblk_t *abp, void *abd, size_t abd_size, int is_fnode,
               uint32_t bn)
{
    size_t maxcnt;
    if (abp->ab_flag & AB_NODES) {
        maxcnt = is_fnode ? FN_MAX_NODE : AS_MAX_NODE;
    } else {
        maxcnt = is_fnode ? FN_MAX_LEAF : AS_MAX_LEAF;
    }
    if (abp->ab_busycnt > maxcnt)
        return -1; /* EINVAL โ€” rejected */
    /* same loop as before, but now provably in-bounds */
    if (abp->ab_flag & AB_NODES) {
        alnode_t *anp = (alnode_t *)abd;
        for (uint32_t i = 0; i < abp->ab_busycnt; i++, anp++) {
            (void)anp->an_nextoff; (void)bn;
        }
    } else {
        alleaf_t *alp = (alleaf_t *)abd;
        for (uint32_t i = 0; i < abp->ab_busycnt; i++, alp++) {
            (void)alp->al_off; (void)bn;
        }
    }
    return 0;
}

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

    printf("=== DF-0857 deterministic OOB proof ===\n");
    printf("struct sizes: alblk=%zu alleaf=%zu alnode=%zu\n",
           sizeof(alblk_t), sizeof(alleaf_t), sizeof(alnode_t));
    printf("data areas : fn_abd=0x%x (%d) as_abd=0x%x (%d)\n",
           FN_ABD_SIZE, FN_ABD_SIZE, AS_ABD_SIZE, AS_ABD_SIZE);
    printf("legit max  : fnode-leaves=%d fnode-nodes=%d "
           "alsec-leaves=%d alsec-nodes=%d\n\n",
           (int)FN_MAX_LEAF, (int)FN_MAX_NODE,
           (int)AS_MAX_LEAF, (int)AS_MAX_NODE);

    /* -------- BUG PRESENT: forged busycnt = 255 on each container -------- */
    uint8_t forged = 255;
    uint32_t bn = 0;   /* never matches any leaf an_nextoff/al_off, so loop
                          runs to completion (worst case) */

    struct {
        const char *name;
        int         is_fnode;
        size_t      container_size;
        size_t      abd_size;
        uint8_t     flag;     /* AB_NODES or 0 (leaf) */
        size_t      expected_oob_past_data;  /* (255*elsize) - abd_size */
        size_t      expected_total_read;     /*  255*elsize            */
    } cases[] = {
        { "fnode leaf",  1, sizeof(fnode_container_t), FN_ABD_SIZE, 0,        2964, 3060 },
        { "fnode node",  1, sizeof(fnode_container_t), FN_ABD_SIZE, AB_NODES, 1944, 2040 },
        { "alsec leaf",  0, sizeof(alsec_container_t), AS_ABD_SIZE, 0,        2580, 3060 },
        { "alsec node",  0, sizeof(alsec_container_t), AS_ABD_SIZE, AB_NODES, 1560, 2040 },
    };

    int any_oob = 0;
    for (int k = 0; k < 4; k++) {
        void *base = poison_alloc(cases[k].container_size, pagesz);
        alblk_t *abp;
        void   *abd;
        if (cases[k].is_fnode) {
            fnode_container_t *fn = (fnode_container_t *)base;
            abp = &fn->fn_ab; abd = fn->fn_abd;
        } else {
            alsec_container_t *as = (alsec_container_t *)base;
            abp = &as->as_ab; abd = as->as_abd;
        }
        abp->ab_flag = cases[k].flag;
        abp->ab_busycnt = forged;

        int poison = 0;
        size_t oob = run_loop(abp, abd, cases[k].abd_size, forged, bn, &poison);
        printf("[BUG] %-12s forged busycnt=%-3u  total read=%zuB  OOB past data=%zuB"
               " (expected %zu)  poison-page hit = %s\n",
               cases[k].name, forged,
               cases[k].expected_total_read, oob, cases[k].expected_oob_past_data,
               poison ? "YES" : "no");
        if (oob > 0) any_oob = 1;
        if (cases[k].expected_oob_past_data != oob) {
            printf("  *** UNEXPECTED OOB extent (expected %zu got %zu)\n",
                   cases[k].expected_oob_past_data, oob);
        }
    }
    printf("\n");

    /* -------- FIXED: bounds-check rejects forged busycnt ---------------- */
    int all_rejected = 1;
    for (int k = 0; k < 4; k++) {
        void *base = poison_alloc(cases[k].container_size, pagesz);
        alblk_t *abp;
        void   *abd;
        if (cases[k].is_fnode) {
            fnode_container_t *fn = (fnode_container_t *)base;
            abp = &fn->fn_ab; abd = fn->fn_abd;
        } else {
            alsec_container_t *as = (alsec_container_t *)base;
            abp = &as->as_ab; abd = as->as_abd;
        }
        abp->ab_flag = cases[k].flag;
        abp->ab_busycnt = forged;

        int rc = run_loop_fixed(abp, abd, cases[k].abd_size, cases[k].is_fnode, bn);
        printf("[FIX] %-12s forged busycnt=%-3u  result = %s\n",
               cases[k].name, forged,
               rc == -1 ? "REJECTED (EINVAL, no loop run)" : "accepted (no OOB)");
        if (rc != -1) all_rejected = 0;
    }

    printf("\n=== SUMMARY ===\n");
    printf("DF_0857_BUG_PRESENT_TOTAL_LEAF_READ_BYTES=3060\n");
    printf("DF_0857_BUG_PRESENT_TOTAL_NODE_READ_BYTES=2040\n");
    printf("DF_0857_BUG_PRESENT_OOB_LEAF_PAST_DATA_FN=2964\n");
    printf("DF_0857_BUG_PRESENT_OOB_NODE_PAST_DATA_FN=1944\n");
    printf("DF_0857_BUG_CONFIRMED=%d\n", any_oob);
    printf("DF_0857_FIX_REJECTS_FORGED_BUSYCNT=%d\n", all_rejected);
    return any_oob ? 0 : 1;
}