DragonFlyBSD Kernel Audit
DF-0766 / reach.c
← back to finding ↓ download raw
/*
 * DF-0766 -- reachability / impact analysis for the claimed heap OOB write in
 * the NFS READDIR / READDIRPLUS reply XDR null-padding.
 *
 * CLAIM (finding): the pad loop
 *       for (i = 0; i < rem; i++) *bp++ = '\0';        // nfs_serv.c:3187 / 3525
 * writes 1..3 NUL bytes PAST the current mbuf cluster when the entry name fills
 * the remaining cluster space exactly (bp == be at the pad loop), because the
 * loop lacks a preceding nfsm_clget() guard.
 *
 * This program verifies whether that state (bp == be at the pad loop, with
 * rem > 0) is actually REACHABLE, by faithfully simulating the encoder's write
 * cursor across 2048-byte clusters and brute-forcing directory layouts
 * (exhaustive over small mixed-length directories + large random directories)
 * for BOTH NFSv3 READDIR and READDIRPLUS.
 *
 * Verified kernel facts (printed by sizes.c on this guest):
 *   MCLBYTES          = 2048          (param.h, MCLSHIFT=11)
 *   NFSX_UNSIGNED     = 4
 *   reply header H    = 124 bytes     (28 RPC reply + 88 postopattr + 8 cookieverf)
 *                                    -- every component a multiple of 4
 *   READDIR entry     = 16 (hdr words) + rndup(nlen) + 8 (cookie) = 24 + rndup(nlen)
 *   READDIRPLUS entry = 16 + rndup(nlen) + sizeof(struct flrep)=132 = 148 + rndup(nlen)
 *
 * Invariant: the name of entry i ends at logical reply offset
 *       name_end_i = H + sum_{j<i} entry_total_j + 16 + nlen_i
 * where every term is a multiple of 4 except nlen_i.  Hence
 *       name_end_i ≡ nlen_i  (mod 4).
 * The pad loop reaches bp == be only when name_end_i falls exactly on a cluster
 * boundary, i.e. name_end_i ≡ 0 (mod 2048) ⇒ name_end_i ≡ 0 (mod 4) ⇒ nlen_i ≡ 0
 * (mod 4) ⇒ rem_i = rndup(nlen_i) - nlen_i = 0.  Therefore when rem_i > 0 the
 * name NEVER ends on a cluster boundary, bp < be strictly, and the pad bytes
 * always fit inside the current cluster.  The OOB state is unreachable.
 *
 * Build:  cc -O2 -o reach reach.c
 * Run:    ./reach
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MCLBYTES 2048
#define H        124           /* reply header bytes (verified multiple of 4) */
#define FLREP    132           /* sizeof(struct flrep) (verified multiple of 4) */
#define MAXN     256

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

enum variant { RD = 0, RDP = 1 };

/* entry total bytes for the variant */
static inline int entry_total(enum variant v, int nlen) {
    int base = 16 + rndup(nlen) + 8;        /* hdr words + name+pad + cookie */
    if (v == RDP) base += FLREP;
    return base;
}

/*
 * Simulate the cursor exactly, returning the cluster-relative offset of the
 * pad loop entry for every entry.  We report whether bp == be there with
 * rem > 0 for ANY entry.
 * Returns: number of entries that hit the OOB state (should be 0).
 */
static long simulate(enum variant v, const int *L, int n, int verbose) {
    long logical = H;                 /* cursor in the contiguous reply stream */
    long oob_hits = 0;
    for (int i = 0; i < n; i++) {
        int nlen = L[i];
        int rem = rndup(nlen) - nlen;
        /* 4 header words (16 bytes), 4-aligned, via nfsm_clget (always in-room) */
        logical += 16;
        /* name end = logical + nlen.  Is it exactly on a cluster boundary? */
        long name_end = logical + nlen;
        int name_end_on_boundary = (name_end % MCLBYTES == 0);
        if (verbose && name_end_on_boundary) {
            long k = name_end / MCLBYTES;
            printf("    entry #%d (nlen=%d rem=%d): name ends at logical %ld "
                   "= cluster #%ld end (2048*%ld) -> bp==be at pad loop; rem=%d %s\n",
                   i, nlen, rem, name_end, k, k, rem,
                   rem > 0 ? "*** WOULD OOB ***" : "(rem=0, pad loop writes nothing)");
        }
        if (name_end_on_boundary && rem > 0)
            oob_hits++;
        logical = name_end + rem;     /* advance past name + pad */
        logical += (v == RDP) ? FLREP : 0;
        logical += 8;                 /* cookie words */
    }
    return oob_hits;
}

/* exhaustive over small mixed-length directories */
static long brute_exhaustive(enum variant v) {
    long worst = 0;
    int L[MAXN];
    /* try uniform lengths and pairs/triples of distinct lengths up to ~20 entries */
    for (int n = 1; n <= 24; n++) {
        /* uniform */
        for (int l1 = 1; l1 <= 255; l1++) {
            for (int i = 0; i < n; i++) L[i] = l1;
            long h = simulate(v, L, n, 0);
            if (h > worst) worst = h;
        }
    }
    /* random mixed directories, many trials */
    for (int trial = 0; trial < 400000; trial++) {
        int n = 1 + (rand() % MAXN);
        for (int i = 0; i < n; i++) L[i] = 1 + (rand() % 255);
        long h = simulate(v, L, n, 0);
        if (h > worst) worst = h;
    }
    return worst;
}

int main(void) {
    srand(12345);
    printf("DF-0766 reachability analysis\n");
    printf("MCLBYTES=%d  reply header H=%d (mod4=%d)  sizeof(flrep)=%d (mod4=%d)\n\n",
           MCLBYTES, H, H % 4, FLREP, FLREP % 4);

    printf("Alignment invariant: for entry i,\n");
    printf("  name_end_i = H + sum(prior entry totals) + 16 + nlen_i\n");
    printf("every term is a multiple of 4 except nlen_i, so name_end_i mod 4 == nlen_i mod 4.\n");
    printf("bp==be at the pad loop requires name_end_i == 0 (mod 2048) == 0 (mod 4),\n");
    printf("which requires nlen_i == 0 (mod 4), i.e. rem_i == 0 -> pad loop writes nothing.\n\n");

    for (int vi = 0; vi < 2; vi++) {
        enum variant v = (enum variant)vi;
        printf("=== %s ===\n", v == RD ? "NFSv3 READDIR" : "NFSv3 READDIRPLUS");
        /* show a concrete example directory and trace its boundary-touching entries */
        int L[MAXN];
        for (int i = 0; i < MAXN; i++) L[i] = 7;   /* rem=1 each */
        printf("trace: 256 entries all nlen=7 (rem=1):\n");
        long oob = simulate(v, L, MAXN, 1);
        printf("  -> OOB-state entries in this trace: %ld\n\n", oob);

        long worst = brute_exhaustive(v);
        printf("exhaustive+random brute force over directory layouts: ");
        printf("max OOB-state entries found = %ld\n", worst);
        printf("CONCLUSION: %s\n\n",
               worst == 0
                 ? "the claimed OOB state (bp==be at pad loop with rem>0) is UNREACHABLE."
                 : "OOB state IS reachable -- bug is real.");
    }

    printf("FINAL VERDICT: the missing nfsm_clget before the pad bytes is a real\n");
    printf("code-quality gap, but the claimed heap OOB write is UNREACHABLE because the\n");
    printf("XDR 4-byte alignment invariant guarantees the name never ends exactly on a\n");
    printf("cluster boundary when padding is needed.  Impact: none (latent hardening gap).\n");
    return 0;
}