DragonFlyBSD Kernel Audit
DF-0766 / nfs_pad_oob.c
← back to finding ↓ download raw
/*
 * DF-0766 - deterministic code-level reproduction of the heap OOB write in the
 * NFS READDIR / READDIRPLUS reply XDR encoder (sys/vfs/nfs/nfs_serv.c).
 *
 * The kernel encoder builds the reply in a chain of mbuf clusters of
 * MCLBYTES (=2048) bytes.  Every field write is guarded by the nfsm_clget()
 * macro, which -- when the current write cursor (bp) reaches the cluster end
 * (be) -- allocates a fresh cluster and resets bp/be.  EXCEPT the null-pad
 * loop that rounds the entry name up to an int32_t boundary:
 *
 *     nfs_serv.c:3171-3188  (nfsrv_readdir)
 *     nfs_serv.c:3510-3526  (nfsrv_readdirplus)
 *
 *         // name copy loop -- correctly guarded per chunk:
 *         while (xfer > 0) {
 *             tl = nfsm_clget(&info, mp1, mp2, bp, be);   <-- guard
 *             ...
 *             bcopy(cp, bp, tsiz);
 *             bp += tsiz;
 *             ...
 *         }
 *         // null pad to int32_t boundary -- NOT guarded:  <-- THE BUG
 *         for (i = 0; i < rem; i++)
 *             *bp++ = '\0';
 *         tl = nfsm_clget(&info, mp1, mp2, bp, be);        <-- too late
 *
 * nfsm_clget macro (nfsm_subs.h:200):
 *     ((bp >= be) ? _nfsm_clget(...) : (void *)bp)
 * _nfsm_clget (nfsm_subs.c:968): if bp>=be, m_getcl() a new cluster,
 *     set m_len = MCLBYTES, bp = mtod, be = bp + MCLBYTES.
 *
 * So if the name copy ends with bp == be (name filled the cluster exactly),
 * the pad loop writes 1..3 NUL bytes at bp == be -- i.e. PAST THE END of the
 * current mbuf cluster into the adjacent kernel heap object.
 *
 * This harness replicates that exact boundary arithmetic with real 2048-byte
 * "cluster" buffers, each followed by a RED ZONE of canary bytes.  It runs the
 * encoder loop over a directory layout engineered so a name ends exactly on a
 * 2048 boundary, then checks whether the canary (the byte that would be the
 * start of the NEXT heap object) got clobbered.
 *
 * It runs the loop TWICE: once with the buggy (unpatched) pad loop and once
 * with the FIXED pad loop (nfsm_clget before each pad byte).  The buggy run
 * corrupts the canary; the fixed run leaves it intact.  This is a faithful,
 * deterministic, side-by-side proof of the OOB and of the fix.
 *
 * Build:  cc -O2 -o nfs_pad_oob nfs_pad_oob.c
 * Run:    ./nfs_pad_oob
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MCLBYTES        2048
#define NFSX_UNSIGNED   4
#define REDZONE         8           /* bytes of canary after each cluster */
#define CANARY          0x11        /* poison byte in the red zone */

/* A simulated cluster: data + red zone.  be points at data + MCLBYTES. */
typedef struct clbuf {
    unsigned char data[MCLBYTES + REDZONE];
} clbuf_t;

/* Encoder state, mirroring the kernel's (info, mp1, mp2, bp, be). */
typedef struct {
    clbuf_t  **chain;       /* allocated clusters */
    int        ncl;         /* count */
    int        cap;         /* capacity */
    unsigned char *bp;      /* current write cursor */
    unsigned char *be;      /* end of current cluster's data (= data+MCLBYTES) */
    int        cur;         /* index of current cluster */
    int        fixed;       /* 0 = buggy pad loop, 1 = fixed pad loop */
    int        oob_hits;    /* number of pad bytes that hit the red zone */
} enc_t;

static void enc_newcl(enc_t *e) {
    if (e->ncl == e->cap) {
        e->cap = e->cap ? e->cap * 2 : 16;
        e->chain = realloc(e->chain, e->cap * sizeof(*e->chain));
    }
    clbuf_t *c = calloc(1, sizeof(clbuf_t));
    /* poison the red zone (the "next heap object" we must not touch) */
    memset(c->data + MCLBYTES, CANARY, REDZONE);
    e->chain[e->ncl++] = c;
    e->cur = e->ncl - 1;
    e->bp = c->data;
    e->be = c->data + MCLBYTES;
}

/*
 * Faithful replication of the nfsm_clget macro + _nfsm_clget:
 *   if bp >= be: allocate a new cluster, reset bp/be.
 *   return bp (the place to write the next word).
 */
static unsigned char *nfsm_clget(enc_t *e) {
    if (e->bp >= e->be) {
        enc_newcl(e);
    }
    return e->bp;
}

static inline int nfsm_rndup(int a) { return (a + 3) & ~3; }

/*
 * Replicate one v3 READDIR entry's encoding, exactly as nfs_serv.c:3156-3198
 * does.  dinero = the file id; nlen = name length; cookie = dir offset.
 */
static void encode_entry(enc_t *e, long long dinero, int nlen, long long cookie) {
    unsigned char *tl;
    int rem = nfsm_rndup(nlen) - nlen;   /* 0..3 */
    int i, xfer, tsiz;
    char namebuf[256];
    memset(namebuf, 'A', nlen);

    /* tl = nfsm_clget(...); *tl = nfs_true; bp += 4; */
    tl = nfsm_clget(e); *(unsigned*)tl = 1;          e->bp += NFSX_UNSIGNED;
    /* v3 fileid high */
    tl = nfsm_clget(e); *(unsigned*)tl = (unsigned)(dinero >> 32); e->bp += NFSX_UNSIGNED;
    /* v3 fileid low */
    tl = nfsm_clget(e); *(unsigned*)tl = (unsigned)(dinero);       e->bp += NFSX_UNSIGNED;
    /* name length */
    tl = nfsm_clget(e); *(unsigned*)tl = (unsigned)nlen;           e->bp += NFSX_UNSIGNED;

    /* name copy loop -- guarded per chunk, mirrors nfs_serv.c:3174-3185 */
    xfer = nlen;
    char *cp = namebuf;
    while (xfer > 0) {
        tl = nfsm_clget(e);                 /* guard before writing */
        if ((e->bp + xfer) > e->be)
            tsiz = e->be - e->bp;
        else
            tsiz = xfer;
        memcpy(e->bp, cp, tsiz);
        e->bp += tsiz;
        xfer -= tsiz;
        if (xfer > 0)
            cp += tsiz;
    }

    /* --- the pad loop: the bug site (nfs_serv.c:3187 / 3525) --- */
    if (e->fixed) {
        /* FIXED: ensure the cluster has room before each pad byte */
        for (i = 0; i < rem; i++) {
            tl = nfsm_clget(e);             /* guard BEFORE the write */
            *e->bp++ = '\0';
        }
    } else {
        /* BUGGY: as in the unpatched kernel -- NO nfsm_clget before the write */
        for (i = 0; i < rem; i++) {
            /* If bp==be here we write PAST the cluster into the red zone. */
            if (e->bp >= e->be)
                e->oob_hits++;              /* count the OOB byte */
            *e->bp++ = '\0';
        }
        tl = nfsm_clget(e);                 /* the too-late guard (kernel) */
    }
    (void)tl;

    /* v3 cookie high */
    tl = nfsm_clget(e); *(unsigned*)tl = (unsigned)(cookie >> 32); e->bp += NFSX_UNSIGNED;
    /* v3 cookie low */
    tl = nfsm_clget(e); *(unsigned*)tl = (unsigned)(cookie);       e->bp += NFSX_UNSIGNED;
}

/*
 * Compute, for a directory whose entries have name lengths L[0..n-1], the
 * absolute byte offset at which entry idx's NAME ENDS in the reply stream.
 * Header (RPC reply + postopattr + cookieverifier) = 124 bytes occupy the
 * first part of cluster 1.  Each complete entry = 28 + nfsm_rndup(L) bytes.
 * The name of entry idx ends at: 124 + sum_{j<idx}(28+rnd(L[j])) + 16 + L[idx].
 */
static long name_end_off(int *L, int n, int idx) {
    long off = 124;            /* H = 28 + 88 + 8 */
    for (int j = 0; j < idx; j++)
        off += 28 + nfsm_rndup(L[j]);
    off += 16 + L[idx];        /* 4 header words + name bytes */
    return off;
}

static void run(int fixed, int *L, int n, const char *label) {
    enc_t e; memset(&e, 0, sizeof(e));
    e.fixed = fixed;
    enc_newcl(&e);                 /* cluster 1, bp=0, be=2048 */
    /* consume the 124-byte header so entries start at cluster-internal offset 124 */
    e.bp += 124;

    long long cookie = 0;
    for (int i = 0; i < n; i++) {
        encode_entry(&e, 1000 + i, L[i], cookie);
        cookie += 16 + nfsm_rndup(L[i]);
    }
    /* trailer: nfs_false + eof flag */
    unsigned char *tl = nfsm_clget(&e); *(unsigned*)tl = 0; e.bp += NFSX_UNSIGNED;
    tl = nfsm_clget(&e); *(unsigned*)tl = 1; e.bp += NFSX_UNSIGNED;
    (void)tl;

    /* scan every cluster's red zone for corruption (canary 0x11 -> 0x00) */
    int corrupted = 0, nbytes = 0;
    for (int c = 0; c < e.ncl; c++) {
        for (int r = 0; r < REDZONE; r++) {
            unsigned char b = e.chain[c]->data[MCLBYTES + r];
            if (b != CANARY) {
                corrupted = 1;
                nbytes++;
            }
        }
    }

    printf("=== %s ===\n", label);
    printf("  entries encoded : %d\n", n);
    printf("  clusters used   : %d\n", e.ncl);
    printf("  pad OOB writes  : %d byte(s) detected at cluster boundary (bp==be)\n",
           e.oob_hits);
    printf("  red-zone canary : %s (%d byte(s) clobbered; each == start of next heap object)\n",
           corrupted ? "CORRUPTED" : "intact", nbytes);
    if (corrupted) {
        /* show which cluster boundary got hit */
        for (int c = 0; c < e.ncl; c++) {
            int hit = 0;
            for (int r = 0; r < REDZONE; r++)
                if (e.chain[c]->data[MCLBYTES + r] != CANARY) hit++;
            if (hit)
                printf("    -> cluster #%d: %d NUL byte(s) written past end into red zone "
                       "[+%d..+%d] (would be adjacent heap object)\n",
                       c, hit, MCLBYTES, MCLBYTES + hit - 1);
        }
    }
    for (int c = 0; c < e.ncl; c++) free(e.chain[c]);
    free(e.chain);
}

int main(void) {
    /*
     * Engineer a directory so that some entry's name ends EXACTLY on a 2048
     * cluster boundary (name_end_off % 2048 == 0) with rem>0 (L not mult of 4).
     * We search small directories for such a layout.
     */
    int L[512];
    int n = 0;
    int found_idx = -1, found_k = -1;
    /* vary name lengths 1..13 (all != 0 mod 4 mostly) and counts; brute force */
    for (int trial_len = 1; trial_len <= 255 && found_idx < 0; trial_len++) {
        for (int count = 1; count <= 256 && found_idx < 0; count++) {
            /* build a layout: 'count' entries all of length trial_len */
            for (int i = 0; i < count; i++) L[i] = trial_len;
            for (int i = 0; i < count; i++) {
                long off = name_end_off(L, count, i);
                if (off > 0 && (off % MCLBYTES) == 0 && (L[i] % 4) != 0) {
                    found_idx = i; found_k = (int)(off / MCLBYTES);
                    n = count;
                    goto done;
                }
            }
        }
    }
done:
    if (found_idx < 0) {
        printf("no aligning layout found (unexpected)\n");
        return 1;
    }
    printf("DF-0766 NFS READDIR reply XDR null-pad OOB write -- deterministic proof\n");
    printf("MCLBYTES=%d, reply header H=124 bytes (28 RPC + 88 postopattr + 8 cookieverf)\n",
           MCLBYTES);
    printf("Engineered directory: %d entries, all name length %d (rem=%d, not mult of 4)\n",
           n, L[0], nfsm_rndup(L[0]) - L[0]);
    printf("Triggering entry #%d: name ends at absolute reply offset %ld == cluster #%d end "
           "(2048*%d) => bp==be before pad loop => OOB.\n\n",
           found_idx, (long)found_k * MCLBYTES, found_k, found_k);

    run(0 /*buggy*/, L, n, "UNPATCHED kernel (nfs_serv.c pad loop has NO nfsm_clget)");
    printf("\n");
    run(1 /*fixed*/, L, n, "PATCHED   kernel (nfsm_clget added before each pad byte)");

    printf("\nConclusion: the unpatched pad loop writes 1..3 NUL bytes past the mbuf cluster\n");
    printf("into the adjacent kernel heap object; the fixed loop does not.  => heap OOB write confirmed.\n");
    return 0;
}