DF-0806 / harness.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 | /* * DF-0806 โ dirfs_readlink off-by-one heap overflow + OOB read * Deterministic userspace harness (faithful transcription of the kernel code). * * BUG LOCATION: sys/vfs/dirfs/dirfs_vnops.c:1328-1334 (dirfs_readlink) * * 1328: buf = kmalloc(uio->uio_resid, M_DIRFS_MISC, M_WAITOK | M_ZERO); * 1329: nlen = readlinkat(pathnp->dn_fd, dnp->dn_name, buf, uio->uio_resid); * 1330: if (nlen == -1 ) { * 1331: error = errno; * 1332: } else { * 1333: error = uiomove(buf, nlen + 1, uio); // copies nlen+1 bytes * 1334: buf[nlen] = '\0'; // writes at index nlen * 1335: ... * * uio->uio_resid flows UNCLAMPED from the user's raw readlink() count via * kern_readlink (sys/kern/vfs_syscalls.c:3211 auio.uio_resid = count). * * When the symlink target length >= uio_resid (== N), POSIX readlinkat returns * nlen == N (exactly bufsiz). Then: * - line 1334: buf[N] = '\0' -> writes 1 byte past the N-byte allocation * (CWE-787 off-by-one heap overflow / OOB write) * - line 1333: uiomove(buf, N+1, uio) -> reads buf[0..N] = N+1 bytes from an * N-byte buffer (CWE-125 1-byte OOB read) * * WHY A HARNESS: dirfs is vkernel64-only โ listed in * sys/platform/vkernel64/conf/files (optional dirfs) but NOT in sys/conf/files, * so it is absent from the running X86_64_GENERIC kernel, and there is no * dirfs.ko in /boot/kernel. It cannot be mounted or triggered on this guest. * The finding explicitly authorizes a deterministic harness fallback. This * program transcribes the exact buggy operations with a guard-page allocator * (mmap + mprotect) so a 1-byte overflow is detected deterministically * (ASan/libasan is not shipped on this guest either). * * The guard-page allocator places the N-byte buffer flush against a PROT_NONE * page, so any access to buf[N] lands in the guard page and faults (SIGSEGV). * The "fixed" path allocates N+1 bytes so buf[N] is the legitimately-writable * last byte and no fault occurs โ proving the fix closes the OOB. * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness (runs vulnerable + fixed for several N) * ./harness <N> (single size) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <unistd.h> #include <sys/mman.h> #include <sys/wait.h> static sigjmp_buf oob_jmp; static volatile sig_atomic_t oob_caught; static void oob_handler(int sig, siginfo_t *si, void *ctx) { (void)sig; (void)si; (void)ctx; oob_caught = 1; siglongjmp(oob_jmp, 1); } /* Install a SEGV/BUS catcher that jumps back to the last sigsetjmp. */ static void install_catcher(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = oob_handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); } /* * Guard-page allocator: returns a pointer to `alloc` writable bytes whose last * byte is flush against a PROT_NONE page. buf[alloc] therefore lands in the * guard page and faults. This emulates kmalloc(alloc) with byte-exact bounds. */ static char *guard_alloc(size_t alloc) { long pgsz = sysconf(_SC_PAGESIZE); if (pgsz <= 0) pgsz = 4096; size_t need = (size_t)pgsz * 2; void *base = mmap(NULL, need, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (base == MAP_FAILED) { perror("mmap"); exit(2); } /* Second page is the guard. */ if (mprotect((char *)base + pgsz, (size_t)pgsz, PROT_NONE) != 0) { perror("mprotect"); exit(2); } /* buf occupies the LAST `alloc` bytes of the first page. */ return (char *)base + (pgsz - alloc); } /* Simulated uiomove: copy `n` bytes from src to a user sink (UIO_READ). */ static void sim_uiomove(const char *src, size_t n, char *sink) { /* Reads src[0..n-1] โ if n > allocation, src[n-1] is an OOB read. */ memcpy(sink, src, n); } /* * Vulnerable transcription of dirfs_readlink lines 1328-1334 for a given N * (uio_resid) and target length >= N. `mode` selects which op to probe: * 'w' = buf[nlen]='\0' (line 1334, OOB write) * 'r' = uiomove(buf, nlen+1) (line 1333, OOB read) * Returns 0 if the op completed without fault, 1 if it faulted (OOB detected). */ static int vulnerable(size_t N, char mode) { size_t nlen = N; /* readlinkat returns N when target length >= bufsiz */ /* line 1328: kmalloc(uio_resid) == exactly N bytes */ char *buf = guard_alloc(N); memset(buf, 'A', N); /* simulate readlinkat filling buf */ char sink[N + 2]; memset(sink, 0, sizeof(sink)); install_catcher(); oob_caught = 0; if (sigsetjmp(oob_jmp, 1) == 0) { if (mode == 'r') { /* line 1333: uiomove(buf, nlen + 1, uio) -> reads N+1 bytes */ sim_uiomove(buf, nlen + 1, sink); } else { /* line 1334: buf[nlen] = '\0' -> writes at index N */ buf[nlen] = '\0'; } return 0; /* no fault โ op was in-bounds */ } return 1; /* faulted โ OOB access confirmed */ } /* * Fixed transcription: kmalloc(uio_resid + 1) and uiomove(buf, nlen). * Same modes; should NEVER fault. */ static int fixed(size_t N, char mode) { size_t nlen = N; /* FIX: kmalloc(uio_resid + 1) == N+1 bytes */ char *buf = guard_alloc(N + 1); memset(buf, 'A', N); /* readlinkat fills first N bytes */ char sink[N + 2]; memset(sink, 0, sizeof(sink)); install_catcher(); oob_caught = 0; if (sigsetjmp(oob_jmp, 1) == 0) { if (mode == 'r') { /* FIX: uiomove(buf, nlen, uio) -> reads exactly N bytes */ sim_uiomove(buf, nlen, sink); } else { /* buf[nlen] = '\0' now writes the (N+1)th byte โ in-bounds */ buf[nlen] = '\0'; } return 0; } return 1; } static int run_case(const char *label, size_t N) { int w_vuln = vulnerable(N, 'w'); int r_vuln = vulnerable(N, 'r'); int w_fix = fixed(N, 'w'); int r_fix = fixed(N, 'r'); printf("[%s] N=%zu\n", label, N); printf(" VULNERABLE dirfs_readlink transcription (kmalloc(N), nlen=N):\n"); printf(" line 1334 buf[nlen]='\\0' : %s\n", w_vuln ? "FAULT (1-byte heap overflow / OOB WRITE confirmed)" : "no fault"); printf(" line 1333 uiomove(buf,N+1) : %s\n", r_vuln ? "FAULT (1-byte OOB READ confirmed)" : "no fault"); printf(" FIXED transcription (kmalloc(N+1), uiomove(buf,nlen)):\n"); printf(" buf[nlen]='\\0' : %s\n", w_fix ? "FAULT (unexpected!)" : "no fault (in-bounds)"); printf(" uiomove(buf,N) : %s\n", r_fix ? "FAULT (unexpected!)" : "no fault (in-bounds)"); int vuln_hit = (w_vuln || r_vuln); int fix_ok = (!w_fix && !r_fix); printf(" => BUG %s; FIX %s\n\n", vuln_hit ? "PRESENT (OOB detected)" : "absent", fix_ok ? "VALID (no OOB)" : "INVALID (still OOB)"); return vuln_hit && fix_ok; } int main(int argc, char **argv) { printf("=== DF-0806 dirfs_readlink off-by-one harness ===\n"); printf("Transcription of sys/vfs/dirfs/dirfs_vnops.c:1328-1334\n"); printf("Guard-page allocator detects any access to buf[N].\n\n"); int all_ok = 1; if (argc > 1) { size_t N = (size_t)strtoul(argv[1], NULL, 0); if (N == 0 || N >= 4000) { fprintf(stderr, "N must be in 1..4095\n"); return 2; } all_ok &= run_case("user-supplied", N); } else { /* Test several kmalloc bucket-relevant sizes. */ size_t sizes[] = { 16, 32, 64, 128, 256 }; size_t i; for (i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) { char label[32]; snprintf(label, sizeof(label), "kmalloc-%zu bucket", sizes[i]); all_ok &= run_case(label, sizes[i]); } } printf("=== SUMMARY ===\n"); printf("Vulnerable code: 1-byte OOB WRITE at buf[N] (CWE-787) AND\n"); printf(" 1-byte OOB READ via uiomove(buf,N+1) (CWE-125)\n"); printf("Fixed code: no OOB (kmalloc(N+1) + uiomove(buf,nlen))\n"); printf("Overall: %s\n", all_ok ? "BUG CONFIRMED + FIX VALIDATED" : "ANOMALY"); return all_ok ? 0 : 1; } |