DragonFlyBSD Kernel Audit
DF-0790 / harness.c
← back to finding ↓ download raw
/*
 * harness.c - Userspace replication of the ATTRLIST walk in
 *             ntfs_ntvattrget() (sys/vfs/ntfs/ntfs_subr.c:196-236).
 *
 * THE BUG (DF-0790)
 * -----------------
 * ntfs_ntvattrget() reads the $ATTRIBUTE_LIST (NTFS_A_ATTRLIST, type 0x20)
 * resident attribute's data into a heap buffer `alpool` (kmalloc(va_datalen))
 * and then walks the list of `struct attr_attrlist` entries looking for the
 * requested attribute in other MFT records:
 *
 *   186:    len = lvap->va_datalen;
 *   187:    alpool = kmalloc(len, M_TEMP, M_WAITOK);
 *   188:    error = ntfs_readntvattr_plain(ntmp, ip, lvap, 0, len, alpool, &len, NULL);
 *   ...
 *   193:    aalp = (struct attr_attrlist *) alpool;
 *   194:    nextaalp = NULL;
 *   196:    for(; len > 0; aalp = nextaalp) {
 *   202:        if (len > aalp->reclen) {
 *   203:            nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *);
 *   204:        } else {
 *   205:            nextaalp = NULL;
 *   206:        }
 *   207:        len -= aalp->reclen;     // <-- NO CHECK that reclen != 0
 *   ...
 *   236:    }
 *
 * `struct attr_attrlist` (sys/vfs/ntfs/ntfs.h:144-154):
 *   u32 al_type; u16 reclen; u8 al_namelen; u8 al_nameoff;
 *   u64 al_vcnstart; u32 al_inumber; u32 reserved; u16 al_index; u16 al_name[1];
 *
 * NTFS_NEXTREC (ntfs.h:273):
 *   #define NTFS_NEXTREC(s, type) ((type)(((caddr_t) s) + (s)->reclen))
 *
 * Two distinct malformed-image cases, both unguarded:
 *
 *   (A) reclen == 0  ->  len -= 0 (unchanged); nextaalp = aalp + 0 == aalp;
 *                        the for() never advances => INFINITE LOOP (CWE-835).
 *                        In the kernel this hangs the calling thread at 100%
 *                        CPU forever; mount(2) or the triggering syscall
 *                        never returns. Local DoS.
 *
 *   (B) reclen > len ->  the else branch sets nextaalp = NULL;
 *                        len -= reclen UNDERFLOWS (size_t) to a huge value;
 *                        next iteration: len > 0 is true, aalp = NULL;
 *                        the `len > aalp->reclen` check dereferences NULL
 *                        => PANIC (NULL deref, CWE-476).
 *
 * WHY A HARNESS
 * -------------
 * The live NTFS trigger requires (1) root to mount the crafted image and
 * (2) reaching ntfs_ntvattrget() with an attribute that is referenced by a
 * corrupted $ATTRIBUTE_LIST but not present inline in the MFT record. On this
 * guest the sibling DF-0786 lockmgr panic ("locking against itself" in
 * ntfs_ntget during directory lookup) races the lookup path; depending on
 * timing the lockmgr panic may fire first. To make the DF-0790 bug
 * DETERMINISTIC and to exercise the fix in isolation, this harness reproduces
 * the EXACT walk logic with a guard page immediately after the ATTRLIST
 * buffer, so any OOB read faults and any reclen==0 spins (caught by an
 * iteration cap that the kernel does NOT have).
 *
 * The harness mirrors the struct layout from sys/vfs/ntfs/ntfs.h.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness {clean|loop|null|oob_reclen} [apply_fix]
 * Exit:   0 = walk completed cleanly (len exhausted, or fix rejected bad input)
 *         1 = iteration cap hit (would be infinite loop in kernel)
 *         2 = SIGSEGV caught (would be OOB read / NULL deref in kernel)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>

#define AAL_BYTES  4096       /* mirror a typical resident ATTRLIST alloc   */
#define ITER_CAP   100000     /* kernel has no cap; this catches inf loops  */

/* Mirror of struct attr_attrlist (sys/vfs/ntfs/ntfs.h:144-154). */
struct attr_attrlist {
    uint32_t al_type;       /* Attribute type                */
    uint16_t reclen;        /* length of this entry          */
    uint8_t  al_namelen;    /* Attribute name len            */
    uint8_t  al_nameoff;    /* Name offset from entry start  */
    uint64_t al_vcnstart;   /* VCN number                    */
    uint32_t al_inumber;    /* Parent ntnode                 */
    uint32_t reserved;
    uint16_t al_index;      /* Attribute index in MFT record */
    uint16_t al_name[1];    /* Name (variable)               */
} __attribute__((packed));

#define NTFS_NEXTREC(s, type) ((type)(((char *)(s)) + (s)->reclen))

/* Mirror of the buggy walk in ntfs_ntvattrget() lines 196-236.
 * Returns 0 on clean completion, 1 if the iteration cap is hit (infinite
 * loop in the kernel), 2 if a guard-page fault is caught by the caller. */
static int buggy_walk(struct attr_attrlist *aalp, size_t len)
{
    struct attr_attrlist *nextaalp = NULL;
    unsigned iters = 0;

    for (; len > 0; aalp = nextaalp) {
        if (++iters > ITER_CAP)
            return 1;                          /* infinite loop */

        if (len > aalp->reclen) {
            nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *);
        } else {
            nextaalp = NULL;
        }
        len -= aalp->reclen;                   /* UNCHECKED: no !=0 guard */

        /* The kernel body continues with NTFS_AALPCMP / ntfs_vgetex etc.
         * For the harness we only care that the walk advances; a real
         * mismatched entry would `continue`, which we mimic by looping. */
    }
    return 0;                                  /* len exhausted cleanly */
}

/* The proposed fix: reject reclen==0 and reclen>remaining before advancing,
 * and require the fixed entry header to fit in the remaining buffer before
 * any field is read. Returns 0 on clean completion, -1 (EINVAL) if a
 * malformed entry is seen. */
static int fixed_walk(struct attr_attrlist *aalp, size_t len)
{
    struct attr_attrlist *nextaalp;
    unsigned iters = 0;
    /* fixed header size (no variable name tail): the kernel check is
     * `sizeof(struct attr_attrlist)` against the remaining buffer; here we
     * use the 26-byte fixed part, matching the C struct minus the trailing
     * flexible al_name[1] member. */
    const size_t hdr = 26;

    for (; len > 0; aalp = nextaalp) {
        if (++iters > ITER_CAP)
            return -1;

        /* DF-0790 fix step 1: the fixed entry header must fit in the
         * remaining buffer before we dereference ANY field of *aalp.
         * (This is what blocks the oob_reclen case -- without it, reading
         *  aalp->reclen on a pointer that straddles the buffer end faults.) */
        if (len < hdr)
            return -1;

        /* DF-0790 fix step 2: reclen must be sane -- at least the header,
         * and not greater than the remaining buffer. This rejects
         * reclen==0 (infinite loop) and reclen>len (size_t underflow ->
         * NULL deref). */
        if (aalp->reclen < hdr)
            return -1;
        if ((size_t)aalp->reclen > len)
            return -1;

        nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *);
        len -= aalp->reclen;
    }
    return 0;
}

/* ---- signal handling so we can catch the OOB / NULL-deref SIGSEGV cleanly */
static sigjmp_buf jb;
static volatile int got_sig;
static void segv_handler(int sig) { (void)sig; got_sig = SIGSEGV; siglongjmp(jb, 1); }

static uint8_t *buf;       /* the synthetic ATTRLIST buffer */

static struct attr_attrlist *fill_buffer(const char *mode, size_t *out_len)
{
    memset(buf, 0, AAL_BYTES);

    /* Entry 0 at offset 0: a $DATA (0x80) attr-list entry pointing at ino 5. */
    struct attr_attrlist *e0 = (struct attr_attrlist *)buf;
    e0->al_type     = 0x80;          /* NTFS_A_DATA */
    e0->al_namelen  = 0;
    e0->al_nameoff  = 0;
    e0->al_vcnstart = 0;
    e0->al_inumber  = 5;
    e0->al_index    = 0;

    size_t len;
    if (strcmp(mode, "clean") == 0) {
        /* one well-formed 32-byte entry, then len runs out -> clean exit */
        e0->reclen = 32;
        len = 32;                     /* exactly one entry, then len==0 */
    } else if (strcmp(mode, "loop") == 0) {
        /* DF-0790 case (A): reclen == 0 -> walk never advances */
        e0->reclen = 0;
        len = 64;                     /* len > 0 so the for() body runs */
    } else if (strcmp(mode, "null") == 0) {
        /* DF-0790 case (B): reclen > len -> else sets nextaalp=NULL,
         * len underflows to huge, next iter dereferences aalp=NULL.
         * We make len small (8) so reclen(=32) > len triggers the else. */
        e0->reclen = 32;
        len = 8;                      /* len < reclen -> NULL-deref path */
    } else if (strcmp(mode, "oob_reclen") == 0) {
        /* Variant: reclen pushes nextaalp to straddle the buffer end so the
         * next iteration's aalp->reclen deref reads the guard page -> OOB.
         * (Same shape as DF-0787's oob_reclen, here on the ATTRLIST walk.)
         * With buf at the page tail, nextaalp = buf + (AAL_BYTES-1); the
         * following aalp->reclen access reads buf + AAL_BYTES + 3 -> fault. */
        e0->reclen = AAL_BYTES - 1;
        len = AAL_BYTES;
    } else {
        fprintf(stderr, "unknown mode '%s'\n", mode);
        exit(3);
    }

    *out_len = len;
    return e0;
}

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr,
            "usage: %s {clean|loop|null|oob_reclen} [apply_fix]\n", argv[0]);
        return 3;
    }
    const char *mode = argv[1];
    int apply_fix = (argc >= 3 && strcmp(argv[2], "apply_fix") == 0);

    /* Allocate the ATTRLIST buffer with a guard page immediately after it,
     * so any OOB read (case oob_reclen, or the NULL-deref wild pointer)
     * faults deterministically -- mimicking the kernel reading past the
     * kmalloc(va_datalen) slab chunk. */
    long pagesz = 4096;
    size_t mapsz = (AAL_BYTES + pagesz - 1) & ~((size_t)pagesz - 1);  /* round up */
    uint8_t *base = mmap(NULL, mapsz + pagesz, PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); return 3; }
    if (mprotect(base + mapsz, pagesz, PROT_NONE) != 0) { perror("mprotect"); return 3; }
    /* place buf at the END of the writable region so the guard page is right
     * after byte AAL_BYTES-1 (truest mirror of kmalloc(AAL_BYTES) on a slab). */
    buf = base + mapsz - AAL_BYTES;

    size_t len;
    struct attr_attrlist *aalp = fill_buffer(mode, &len);

    /* install SIGSEGV handler */
    struct sigaction sa, oldsa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = segv_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, &oldsa);

    got_sig = 0;
    int rc;
    if (sigsetjmp(jb, 1) == 0) {
        if (apply_fix)
            rc = fixed_walk(aalp, len);
        else
            rc = buggy_walk(aalp, len);
    } else {
        rc = 2;                                /* SIGSEGV caught */
    }
    sigaction(SIGSEGV, &oldsa, NULL);

    const char *verdict;
    switch (rc) {
        case 0:  verdict = "clean exit (ATTRLIST exhausted)"; break;
        case 1:  verdict = "ITERATION CAP HIT -> would be infinite loop in kernel"; break;
        case 2:  verdict = "SIGSEGV -> NULL deref / OOB read in kernel"; break;
        case -1: verdict = "FIX REJECTED malformed entry (EINVAL in kernel)"; break;
        default: verdict = "unknown"; break;
    }
    printf("mode=%-11s apply_fix=%-3d  -> rc=%d  %s\n",
           mode, apply_fix, rc, verdict);
    return (rc == 0 || rc == -1) ? 0 : rc;
}