DF-0844 / dirhash_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 | /* * DF-0844 โ Userspace harness replicating the dirhash-build OOB read. * * Reproduces the missing bounds check in ufsdirhash_build() * (sys/vfs/ufs/ufs_dirhash.c lines 200-216): * * ep = (struct direct *)((char *)bp->b_data + (pos & bmask)); * if (ep->d_reclen == 0 || ep->d_reclen > * DIRBLKSIZ - (pos & (DIRBLKSIZ - 1))) { * // Corrupted directory โ REJECTED * goto fail; * } * // *** MISSING: d_reclen >= DIRSIZ(NEWDIRFMT, ep) *** * if (ep->d_ino != 0) { * slot = ufsdirhash_hash(dh, ep->d_name, ep->d_namlen); // OOB READ * } * * The existing check only validates the entry fits within its 512-byte * DIRBLKSIZ chunk. It does NOT validate that d_reclen is large enough to * hold d_name (i.e. d_reclen >= DIRSIZ(0, ep)). A crafted entry with * d_reclen=8 (passes the chunk check when at chunk offset 504) but * d_namlen=255 causes ufsdirhash_hash โ fnv_32_buf to read 255 bytes * from ep->d_name, which extends past the buffer bp->b_data into kernel * heap โ a CWE-125 out-of-bounds read. * * This harness places the crafted entry at the exact tail of a page, * with a PROT_NONE guard page immediately after. When the replicated * dirhash loop reads ep->d_name (255 bytes), it faults into the guard * page โ SIGSEGV = deterministic proof of the OOB read. * * Build: cc -O2 -o dirhash_oob dirhash_oob.c * Run: ./dirhash_oob (expects SIGSEGV = OOB confirmed) * ./dirhash_oob --fixed (expects clean exit = fix rejects entry) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #include <unistd.h> #include <stdint.h> /* ---- Constants from sys/vfs/ufs/dir.h (DragonFlyBSD) ---- */ #define DEV_BSIZE 512 #define DIRBLKSIZ DEV_BSIZE /* 512 */ #define MAXNAMLEN 255 #define DIRALIGN 4 /* On-disk directory entry (little-endian DragonFly). */ struct direct { uint32_t d_ino; /* 0 */ uint16_t d_reclen; /* 4 */ uint8_t d_type; /* 6 */ uint8_t d_namlen; /* 7 */ char d_name[MAXNAMLEN + 1]; /* 8 */ }; /* offsetof(struct direct, d_name) = 8 */ #define DIRECT_HDRSZ 8 #define DIRECTSIZ(namlen) \ ((DIRECT_HDRSZ + ((namlen)+1) + 3) & ~3) #define DIRSIZ(dp) DIRECTSIZ((dp)->d_namlen) /* ---- FNV-1a hash (from sys/libkern/fnv_hash.h) ---- */ #define FNV1_32_INIT 0x811c9dc5U static uint32_t fnv_32_buf(const void *buf, size_t len, uint32_t hval) { const uint8_t *s = (const uint8_t *)buf; while (len--) hval *= 0x01000193U, hval ^= *s++; return hval; } /* Replicated from ufsdirhash_hash() */ static volatile uint32_t g_sink; static uint32_t dirhash_hash(const char *name, int namelen) { uint32_t hash; hash = fnv_32_buf(name, namelen, FNV1_32_INIT); g_sink = hash; /* prevent DCE */ return hash; } /* ---- Signal handler for OOB detection ---- */ static sigjmp_buf oob_jmp; static volatile int oob_triggered = 0; static void sighandler(int sig) { oob_triggered = 1; siglongjmp(oob_jmp, 1); } /* * Replicate the dirhash-build entry-processing loop for a single entry * at the tail of a buffer. Returns 0 if entry accepted & hashed (no OOB), * -1 if entry rejected by the bounds check. */ static int process_entry(const struct direct *ep, int fixed_check) { /* The EXISTING check from ufs_dirhash.c:201-206 */ if (ep->d_reclen == 0 || ep->d_reclen > DIRBLKSIZ - (DIRBLKSIZ - 8 /* simulated chunk tail */)) { return -1; } /* THE FIX: d_reclen >= DIRSIZ(ep) โ currently MISSING in the kernel */ if (fixed_check && ep->d_reclen < DIRSIZ(ep)) { return -1; } /* ufsdirhash_build line 207-209: if d_ino != 0, hash the name */ if (ep->d_ino != 0) { /* This reads ep->d_namlen bytes from ep->d_name. * If d_name extends past the buffer โ OOB READ. */ uint32_t h = dirhash_hash(ep->d_name, ep->d_namlen); g_sink ^= h; /* use result */ } return 0; } int main(int argc, char **argv) { int fixed = (argc > 1 && strcmp(argv[1], "--fixed") == 0); long pagesize = (long)getpagesize(); if (pagesize <= 0) pagesize = 4096; /* * Allocate two consecutive pages. The first page holds the * directory data buffer at its tail; the second page is a * PROT_NONE guard. The crafted entry's d_name will land in * the guard page โ SIGSEGV when read. */ size_t mapsz = (size_t)pagesize * 2; char *base = mmap(NULL, mapsz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 2; } char *guard = base + pagesize; if (mprotect(guard, pagesize, PROT_NONE) != 0) { perror("mprotect"); return 2; } /* * Place the crafted entry at the tail of the first page. * * We want the entry positioned so that: * - pos & (DIRBLKSIZ-1) = 504 (so d_reclen <= 512-504 = 8) * - ep->d_name starts exactly at the page boundary (guard page) * * Entry header is 8 bytes (DIRECT_HDRSZ). If we place ep at * page_offset = pagesize - 8, then d_name starts at pagesize, * which is the first byte of the guard page. * * For the chunk check: pos & (DIRBLKSIZ-1) must be 504. * (pagesize - 8) & 511 = ? * pagesize=4096: 4088 & 511 = 4088 mod 512 = 4088 - 7*512 = 504. โ */ int entry_pageoff = pagesize - DIRECT_HDRSZ; /* e.g. 4088 */ int chunk_off = entry_pageoff & (DIRBLKSIZ - 1); printf("DF-0844 dirhash OOB harness\n"); printf(" pagesize = %ld\n", pagesize); printf(" entry offset = %d (page-relative)\n", entry_pageoff); printf(" chunk offset = %d\n", chunk_off); printf(" max d_reclen = %d (DIRBLKSIZ - chunk_off)\n", DIRBLKSIZ - chunk_off); struct direct *ep = (struct direct *)(base + entry_pageoff); /* Craft the malformed entry: * d_reclen = 8 โ passes chunk check (8 <= 8) * d_namlen = 255 โ but entry only holds 0 name bytes! * d_ino = 1 โ non-zero โ triggers ufsdirhash_hash * d_type = DT_REG(8) * * ep->d_name starts at base + pagesize = guard page. * dirhash_hash will read 255 bytes from the guard page โ OOB. */ ep->d_ino = 1; ep->d_reclen = 8; ep->d_type = 8; /* DT_REG */ ep->d_namlen = MAXNAMLEN; /* 255 */ printf(" crafted entry: d_ino=%u d_reclen=%u d_type=%u d_namlen=%u\n", ep->d_ino, ep->d_reclen, ep->d_type, ep->d_namlen); printf(" DIRSIZ(ep) = %u (d_reclen %u < DIRSIZ โ MALFORMED)\n", DIRSIZ(ep), ep->d_reclen); printf(" d_name starts at offset %d = %s\n", entry_pageoff + DIRECT_HDRSZ, (entry_pageoff + DIRECT_HDRSZ == pagesize) ? "GUARD PAGE BOUNDARY" : "inside page"); printf(" mode = %s\n", fixed ? "FIXED (with DIRSIZ check)" : "BUGGY (no DIRSIZ check)"); /* Install SIGSEGV handler to catch the OOB read cleanly */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sighandler; sa.sa_flags = SA_NODEFER; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); printf("\nProcessing entry (replicating ufsdirhash_build loop)...\n"); if (sigsetjmp(oob_jmp, 1) == 0) { int rc = process_entry(ep, fixed); if (rc == -1) { printf("RESULT: Entry REJECTED by bounds check (this is the FIX behavior).\n"); printf(" The malformed entry was correctly detected and rejected.\n"); printf(" No OOB read occurred.\n"); } else { printf("RESULT: Entry ACCEPTED and hashed successfully (no OOB in this run).\n"); printf(" WARNING: d_name was within valid memory by chance.\n"); } } else { /* SIGSEGV/SIGBUS caught = OOB read into guard page */ printf("RESULT: *** OOB READ CONFIRMED ***\n"); printf(" fnv_32_buf(ep->d_name, 255) read past the buffer into the guard page.\n"); printf(" The missing d_reclen >= DIRSIZ(ep) check allowed a 255-byte\n"); printf(" out-of-bounds read from the entry's d_name field.\n"); printf(" In the kernel this reads %d bytes of adjacent kernel heap.\n", ep->d_namlen); } munmap(base, mapsz); return oob_triggered ? 0 : (fixed ? 0 : 1); } |