DF-0917 / 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 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | /* * DF-0917 โ Deterministic proof of the use-after-free pattern in the * FUSE IPC transaction-completion path. * * The finding (sys/vfs/fuse/fuse_ipc.c:112-270, exercised via * sys/vfs/fuse/fuse_device.c:118-223) is a RACE: the /dev/fuse read/write * completion path removes the fuse_ipc (fip) from the reply/request TAILQ * under ipc_lock, DROPS the lock, and then dereferences fip (fip->reply, * fip->request.buf via fuse_in(fip), and fip->done via * fuse_ipc_test_and_set_replied) WITHOUT holding a reference on fip. * Concurrently the tx originator (sleeping in fuse_ipc_wait) may TIME OUT * (~35s), at which point fuse_ipc_remove()+fuse_ipc_set_replied()+return * ETIMEDOUT is followed by fuse_ipc_tx:270 fuse_ipc_put(fip) which drops the * LAST reference and FREES fip โ while the device path is still mid-access. * * The live race window is sub-microsecond (it requires the tx waiter's final * 5*hz tsleep to return EWOULDBLOCK at the precise instant the daemon's * write syscall holds fip between lock-drop and set_replied). Winning it * live needs many thousands of 35-second attempts. * * This harness DETERMINISTICALLY reproduces the *pattern* (option (b) in the * verifier's playbook: a code-level harness reproducing the * drop-lock-then-access-fip logic). It models the two competing paths with * pthreads and forces the dangerous interleaving with a barrier placed * exactly where the kernel drops ipc_lock (fuse_device.c:197) before * touching fip again (fuse_device.c:205-219). If the access-after-free is * real, the device-path thread observes the poison marker written by the * timeout/free path โ proving the UAF. * * Build the UNFIXED model (reproduces the UAF): * cc -O2 -o harness harness.c -lpthread * Build the FIXED model (applies the fix.diff refcount_hold-across-window): * cc -O2 -DFIXED -o harness_fixed harness.c -lpthread * Run: ./harness -> "UAF CONFIRMED" (exit 0) * ./harness_fixed -> "NO UAF (fix holds ref across window)" (exit 0) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <stdatomic.h> /* ---- minimal model of the kernel structures (fuse.h:129-145) ---- */ struct fuse_buf { void *buf; size_t len; }; struct fuse_ipc { void *fmp; /* fuse_mount* */ struct fuse_buf request; struct fuse_buf reply; void *request_entry; void *reply_entry; atomic_uint refcnt; unsigned long unique; int sent; int done; }; /* poison marker written into a freed fip, like the kernel slab's * WEIRD_ADDR (0xdeadc0de) INVARIANTS poisoning of freed fuse_ipc slots * (kern_slaballoc.c). The device path detecting this == observing a UAF. */ #define POISON 0xdeadc0de static pthread_mutex_t ipc_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_barrier_t b1, b2; /* two barriers => deterministic interleaving */ static struct fuse_ipc *g_fip; static int g_in_list; /* model of fuse_ipc_get (fuse_ipc.c:91) */ static struct fuse_ipc *ipc_get(void) { struct fuse_ipc *fip = calloc(1, sizeof(*fip)); atomic_store(&fip->refcnt, 1); fip->request.buf = (void*)0x1000; fip->request.len = 64; return fip; } /* model of fuse_ipc_put (fuse_ipc.c:110) โ frees on last ref */ static void ipc_put(struct fuse_ipc *fip) { if (atomic_fetch_sub(&fip->refcnt, 1) == 1) { /* kernel: fuse_buf_free + objcache_put -> slab frees the slot */ /* mark every field with POISON so a later deref is detectable */ unsigned int *p = (unsigned int *)fip; size_t i; for (i = 0; i < sizeof(*fip)/sizeof(*p); i++) p[i] = POISON; /* NOTE: we deliberately do NOT free() โ keep the slot so the * device thread can observe the poisoned contents, exactly as * a freed-then-poisoned kernel slab slot would look before * reallocation. */ } } /* ---- Thread B: the tx originator's TIMEOUT path ---- * models fuse_ipc_wait EWOULDBLOCK final-retry (fuse_ipc.c:179-195) + * fuse_ipc_tx:268-271 error-put. */ static void *tx_waiter_timeout(void *arg) { (void)arg; /* B1: wait until the device thread has removed fip from the list and * released ipc_lock (mirrors fuse_device.c:193+197). */ pthread_barrier_wait(&b1); /* === the kernel drops the ref here on timeout === */ /* fuse_ipc_remove: lock, find fip in heads (no-op, device already * removed it), unlock */ pthread_mutex_lock(&ipc_lock); g_in_list = 0; /* already removed by device thread */ pthread_mutex_unlock(&ipc_lock); /* fuse_ipc_set_replied(fip) -- writes fip->done */ /* fuse_ipc_tx: error -> fuse_ipc_put(fip) -> LAST ref -> FREE */ ipc_put(g_fip); printf("[tx-waiter] timed out: removed+replied+put fip=%p (NOW FREED)\n", (void*)g_fip); /* B2: signal the device thread that fip has been freed, so its * subsequent access is deterministically after the free. */ pthread_barrier_wait(&b2); return NULL; } /* ---- Thread A: the daemon /dev/fuse write completion path ---- * models fuse_device_write (fuse_device.c:164-223). */ static void *device_write(void *arg) { (void)arg; struct fuse_ipc *fip; struct fuse_buf fb = { (void*)0x2000, 48 }; /* fuse_device.c:190-197 */ pthread_mutex_lock(&ipc_lock); fip = g_fip; /* TAILQ find by unique */ if (fip && g_in_list) { g_in_list = 0; /* TAILQ_REMOVE reply_head */ #ifdef FIXED /* === THE FIX (fix.diff): take a reference BEFORE dropping the * lock, so the tx-waiter timeout put cannot free fip while the * device path still dereferences it. */ atomic_fetch_add(&fip->refcnt, 1); #endif } pthread_mutex_unlock(&ipc_lock); /* <<< LOCK DROPPED (fuse_device.c:197) */ if (!fip) { printf("[device] fip not found -> ENOMSG (no UAF this time)\n"); return NULL; } /* === FROM HERE the kernel touches fip with NO lock and NO ref === * B1 releases the tx-waiter to free fip; B2 guarantees the free is * complete before we access fip (deterministic worst-case). */ pthread_barrier_wait(&b1); pthread_barrier_wait(&b2); /* fuse_device.c:205 */ fip->reply = fb; /* fuse_device.c:206 */ void *in = fip->request.buf; /* fuse_in(fip) */ /* fuse_device.c:212 */ (void)in; /* fuse_device.c:219 */ /* fuse_ipc_test_and_set_replied(fip): fip->done */ /* detect whether fip was freed (POISON'd) underneath us */ int uaf = (fip->request.len == POISON || *(unsigned int *)&fip->done == POISON || *(unsigned int *)&fip->request.buf == POISON); if (uaf) { printf("[device] *** UAF CONFIRMED *** accessed fip=%p AFTER it was " "freed by the tx-waiter timeout:\n", (void*)fip); printf(" fip->request.buf = %p (poisoned? %s)\n", fip->request.buf, *(unsigned int*)&fip->request.buf == POISON ? "YES" : "no"); printf(" fip->request.len = 0x%zx (poisoned? %s)\n", fip->request.len, (unsigned)fip->request.len == POISON ? "YES" : "no"); printf(" fip->done = 0x%x (poisoned? %s)\n", fip->done, (unsigned)fip->done == POISON ? "YES" : "no"); printf(" => daemon wrote fip->reply + read fip->request.buf " "into a FREED fuse_ipc slab slot (16B write + ptr read UAF).\n"); #ifdef FIXED atomic_fetch_sub(&fip->refcnt, 1); /* mirror put */ free(g_fip); return NULL; /* unreachable: FIXED model never UAFs */ #else free(g_fip); return (void*)1; #endif } #ifdef FIXED printf("[device] (FIXED model) accessed fip=%p cleanly: ref held across " "window (refcnt=%u) -> tx-waiter put could not free it.\n", (void*)fip, atomic_load(&fip->refcnt)); atomic_fetch_sub(&fip->refcnt, 1); /* mirror the fix's fuse_ipc_put */ #else printf("[device] accessed fip=%p cleanly (timeout did not fire in window)\n", (void*)fip); #endif free(g_fip); return NULL; } int main(void) { pthread_t ta, tb; void *ra = NULL; g_fip = ipc_get(); g_in_list = 1; pthread_barrier_init(&b1, NULL, 2); pthread_barrier_init(&b2, NULL, 2); printf("DF-0917 deterministic UAF harness\n"); printf("modeling fuse_device_write (fuse_device.c:190-219) vs\n"); printf(" fuse_ipc_wait timeout+put (fuse_ipc.c:179-195,266-271)\n"); printf("fip=%p refcnt=1 (single owner, no extra ref held across lock-drop)\n\n", (void*)g_fip); pthread_create(&ta, NULL, device_write, NULL); pthread_create(&tb, NULL, tx_waiter_timeout, NULL); pthread_join(ta, &ra); pthread_join(tb, NULL); pthread_barrier_destroy(&b1); pthread_barrier_destroy(&b2); printf("\n"); #ifdef FIXED (void)ra; printf("RESULT: FIXED model โ NO UAF. The refcount held across the lock-drop\n"); printf("window (refcount_acquire under ipc_lock + fuse_ipc_put after access)\n"); printf("prevents the tx-waiter's timeout put from freeing fip mid-access.\n"); printf("This is exactly the fix in fix.diff.\n"); return 0; #else if (ra == (void*)1) { printf("RESULT: USE-AFTER-FREE reproduced deterministically.\n"); printf("The device completion path dereferences fip after the tx " "timeout freed it;\nthis is the exact pattern the finding cites " "(no refcount held across the lock-drop window).\n"); return 0; } printf("RESULT: no UAF observed (unexpected for this forced interleaving).\n"); return 1; #endif } |