DF-0918 / harness.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 | /* * DF-0918 โ Deterministic proof of the NULL-ohd / KKASSERT(ohd) panic in * fuse_ipc_wait()'s early-return path. * * THE BUG (sys/vfs/fuse/fuse_ipc.c:158-204, via fuse_device.c:99-116 + * fuse_ipc.c:246-287): * * fuse_ipc_wait() has THREE early-return paths that test the `replied` * flag (fip->done) and `return 0` WITHOUT re-checking the mount's `dead` * flag and WITHOUT verifying that fip->reply.buf was populated: * * fuse_ipc.c:169 if (fuse_ipc_test_replied(fip)) return 0; * fuse_ipc.c:173 if (fuse_ipc_test_replied(fip)) return 0; (post-interlock) * (the goto-again loop at :171/:184 re-runs :172-173 up to 6x, widening * the window) * * fuse_device_clear() (fuse_device.c:99-116) โ invoked by the daemon * reader (fuse_device_read:137) when it observes `dead` during teardown โ * walks fmp->reply_head and for every pending fip calls * fuse_ipc_test_and_set_replied(fip) + wakeup(fip) WITHOUT assigning * fip->reply.buf. fip->reply.buf stays NULL (set to NULL by fuse_ipc_get * at fuse_ipc.c:104). fuse_mount_kill (fuse_vfsops.c:65-76) has ALREADY * set dead=1 by the time fuse_device_clear runs. * * RACE: a tx waiter (fuse_ipc_tx -> fuse_ipc_wait) that has passed the * line-163 dead check (dead was 0 then), but is between :169 and the * line-198 post-tsleep dead recheck, can observe `replied==1` at :169 or * :173 (set by fuse_device_clear underneath it) and `return 0`. Back in * fuse_ipc_tx: * * fuse_ipc.c:274 ohd = fuse_out(fip); // == fip->reply.buf == NULL * fuse_ipc.c:275 KKASSERT(ohd); // PANIC (INVARIANTS ON) * // or, INVARIANTS OFF: * fuse_ipc.c:276 error = ohd->error; // NULL deref -> SIGSEGV * * The live race window is sub-microsecond (tx waiter between tsleep_interlock * and the replied check). Winning it live needs many thousands of attempts * against a 5*hz tsleep. This harness DETERMINISTICALLY reproduces the * pattern (option (b) in the verifier playbook). * * Build the UNFIXED model (reproduces the NULL-ohd primitive): * cc -O2 -o harness harness.c -lpthread * Build the FIXED model (applies fix.diff's dead-recheck-after-replied): * cc -O2 -DFIXED -o harness_fixed harness.c -lpthread * Run: ./harness -> "NULL-ohd PRIMITIVE CONFIRMED" (exit 0) * ./harness_fixed -> "NO NULL-ohd (fix rechecks dead)" (exit 0) */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <stdatomic.h> struct fuse_buf { void *buf; size_t len; }; struct fuse_mount { atomic_int dead; }; struct fuse_ipc { struct fuse_mount *fmp; struct fuse_buf request; struct fuse_buf reply; /* fuse_device_clear leaves .buf = NULL */ atomic_int done; /* `replied` flag (fip->done) */ }; static pthread_barrier_t b1, b2; static struct fuse_mount g_fmp; static struct fuse_ipc *g_fip; static int test_dead(struct fuse_mount *f) { return atomic_load(&f->dead); } static int test_replied(struct fuse_ipc *f){ return atomic_load(&f->done); } static int tas_replied(struct fuse_ipc *f) { return atomic_compare_exchange_strong(&f->done, &(int){0}, 1); } static void set_dead(struct fuse_mount *f) { atomic_store(&f->dead, 1); } static struct fuse_ipc *ipc_get(struct fuse_mount *fmp) { struct fuse_ipc *fip = calloc(1, sizeof(*fip)); fip->fmp = fmp; fip->request.buf = (void*)0x1000; fip->reply.buf = NULL; /* <<< the dangerous initial state */ atomic_init(&fip->done, 0); return fip; } /* Thread B: models fuse_mount_kill (sets dead=1) followed by the daemon * reader waking and running fuse_device_clear (sets replied=1 on every * pending fip WITHOUT populating reply.buf, then wakes the waiter). * * In the real kernel fuse_mount_kill runs BEFORE fuse_device_clear, so by * the time replied is set, dead is already 1. */ static void *kill_and_clear(void *arg) { (void)arg; /* B1: wait until the tx waiter has passed its line-163 dead check * (dead was 0) and is sitting right before its line-169/173 replied * test โ the race window. */ pthread_barrier_wait(&b1); /* === fuse_mount_kill (fuse_vfsops.c:69) === */ set_dead(&g_fmp); /* === fuse_device_clear (fuse_device.c:111-115) === * reply.buf is NOT assigned here โ only fuse_device_write does that * (fuse_device.c:205). It stays NULL. */ tas_replied(g_fip); /* wakeup(fip) โ modelled by the b2 barrier */ /* B2: release the tx waiter to observe replied=1 (and, in FIXED, dead=1). */ pthread_barrier_wait(&b2); return NULL; } /* Thread A: fuse_ipc_wait() early-return -> fuse_ipc_tx:274-275. */ static void *tx_waiter(void *arg) { (void)arg; long rc = 0; void *ohd; /* fuse_ipc.c:163 โ dead is 0 at this point (race not yet triggered) */ if (test_dead(g_fip->fmp)) { rc = -1; goto out; } /* RACE WINDOW: kill_and_clear sets dead=1 AND replied=1 underneath us */ pthread_barrier_wait(&b1); pthread_barrier_wait(&b2); /* fuse_ipc.c:169 (and :173 โ same shape) */ #ifdef FIXED if (test_replied(g_fip)) { /* THE FIX (fix.diff): recheck dead; fuse_device_clear implies * dead=1 (fuse_mount_kill ran first). */ if (test_dead(g_fip->fmp)) { printf("[tx-waiter] replied set by teardown + dead=1 -> ENOTCONN (fix)\n"); rc = -2; /* ENOTCONN */ goto out; } printf("[tx-waiter] replied set by real reply + dead=0 -> return 0\n"); rc = 0; goto out; } #else if (test_replied(g_fip)) { /* UNFIXED: trust replied blindly, return 0. */ printf("[tx-waiter] replied set -> return 0 (NO dead recheck)\n"); /* fall through to fuse_ipc_tx:274-275 */ ohd = g_fip->reply.buf; /* fuse_out(fip) == NULL */ if (ohd == NULL) { printf("[tx-waiter] *** NULL-ohd CONFIRMED *** fuse_out(fip)=NULL;\n"); printf(" fuse_ipc.c:275 KKASSERT(ohd) PANIC (INVARIANTS) or\n"); printf(" fuse_ipc.c:276 ohd->error NULL-deref.\n"); rc = 1; } goto out; } #endif out: if (rc == 1) printf("[tx-waiter] RESULT: primitive reproduced (NULL fuse_out -> panic).\n"); else if (rc == -2) printf("[tx-waiter] RESULT: fix returned ENOTCONN (no NULL deref).\n"); return (void*)rc; } int main(void) { pthread_t ta, tb; void *ra = NULL; atomic_init(&g_fmp.dead, 0); /* mount alive at start */ g_fip = ipc_get(&g_fmp); pthread_barrier_init(&b1, NULL, 2); pthread_barrier_init(&b2, NULL, 2); printf("DF-0918 deterministic NULL-ohd harness\n"); printf("modeling fuse_ipc_wait early-return (fuse_ipc.c:163,169,173)\n"); printf(" vs fuse_mount_kill+fuse_device_clear (fuse_vfsops.c:69 + fuse_device.c:111-115)\n"); printf(" + fuse_ipc_tx:274-275 (ohd = fuse_out(fip); KKASSERT(ohd))\n"); printf("fip=%p reply.buf=%p (NULL by design, fuse_ipc.c:104)\n\n", (void*)g_fip, g_fip->reply.buf); #ifdef FIXED printf("(FIXED model: waiter rechecks dead after replied โ fix.diff)\n\n"); #else printf("(UNFIXED model: waiter trusts replied without dead recheck)\n\n"); #endif pthread_create(&ta, NULL, tx_waiter, NULL); pthread_create(&tb, NULL, kill_and_clear, NULL); pthread_join(ta, &ra); pthread_join(tb, NULL); pthread_barrier_destroy(&b1); pthread_barrier_destroy(&b2); printf("\n"); #ifdef FIXED if (ra == (void*)-2) { printf("RESULT: FIXED model โ NO NULL-deref. dead-recheck after replied\n"); printf("(fix.diff) returns ENOTCONN when fuse_device_clear set replied\n"); printf("during teardown, so fuse_ipc_tx:275 never derefs NULL ohd.\n"); free(g_fip); return 0; } printf("RESULT: FIXED model โ unexpected ra=%p\n", ra); free(g_fip); return 1; #else if (ra == (void*)1) { printf("RESULT: NULL-ohd PRIMITIVE reproduced deterministically.\n"); printf("The early-return trusted `replied` without rechecking dead;\n"); printf("fuse_device_clear set replied with reply.buf still NULL, so\n"); printf("fuse_ipc.c:275 KKASSERT(ohd) fires (panic on default GENERIC).\n"); free(g_fip); return 0; } printf("RESULT: no primitive observed (unexpected for this forced interleaving).\n"); free(g_fip); return 1; #endif } |