DF-0766 / nfs_pad_oob.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | /* * 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; } |