DF-4284 / midi_unload_model.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 182 183 184 185 186 187 188 189 190 | /* * midi_unload_model.c -- userland harness modeling sys/dev/sound/midi/midi.c * * midi_unload() [midi.c:1457-1495]: * TAILQ_FOREACH(m, &midi_devs, link) { * lockmgr(&m->lock, LK_EXCLUSIVE); * ... * retval = midi_destroy(m, 1); <-- midi.c:1474 * } * midi_destroy() [midi.c:1418-1438]: * ... TAILQ_REMOVE(&midi_devs, m, link); <-- midi.c:1429 * ... kfree(m, M_MIDI); <-- midi.c:1436 * * TAILQ_FOREACH advances with `m = TAILQ_NEXT(m, link)` AFTER the loop body * -- i.e. it dereferences m->link.tqe_next of the object that midi_destroy() * just removed from the list and freed. That is a use-after-free read; the * value read is whatever now occupies the freed slab slot. Any same-size * kernel allocation (M_MIDI zone reuse via attacker-groomable kmalloc churn) * controls it, so the unload loop then operates on an attacker-influenced * pointer (lockmgr(&m->lock, ...), m->busy, ... ). * * TAILQ macros below are verbatim from sys/sys/queue.h (TRACEBUF/QUEUEDEBUG * and _DIAGNOSTIC bits, inactive in kernel builds, omitted). * * Modes: * reuse - after each free, a same-size "attacker" chunk immediately * reoccupies the memory with a forged link.tqe_next * (0x4141414141414141) => loop follows ATTACKER pointer. * stale - plain free(); tqe_next reads allocator freelist garbage * (still a UAF read, uncontrolled). * fixed - fix.diff semantics: stash TAILQ_NEXT before destroying; * clean traversal of all devices. * * Exit codes: 0 = clean, 99 = SIGSEGV at the UAF-read pointer. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> /* canonical (bit47 extension zero) unmapped attacker pointer */ #define FAKE_NEXT ((struct snd_midi *)0x0000414141414140ULL) /* ---------- sys/sys/queue.h TAILQ (verbatim, kernel build config) ----- */ #define TAILQ_HEAD(name, type) struct name { struct type *tqh_first; \ struct type **tqh_last; } #define TAILQ_ENTRY(type) struct { struct type *tqe_next; \ struct type **tqe_prev; } #define TAILQ_FIRST(head) ((head)->tqh_first) #define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) #define TAILQ_INIT(head) do { \ TAILQ_FIRST((head)) = NULL; \ (head)->tqh_last = &TAILQ_FIRST((head)); \ } while (0) #define TAILQ_REMOVE(head, elm, field) do { \ if ((TAILQ_NEXT((elm), field)) != NULL) \ TAILQ_NEXT((elm), field)->field.tqe_prev = \ (elm)->field.tqe_prev; \ *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \ } while (0) #define TAILQ_FOREACH(var, head, field) \ for ((var) = TAILQ_FIRST((head)); \ (var); \ (var) = TAILQ_NEXT(var, field)) #define TAILQ_INSERT_TAIL(head, elm, field) do { \ (elm)->field.tqe_next = NULL; \ (elm)->field.tqe_prev = (head)->tqh_last; \ *(head)->tqh_last = (elm); \ (head)->tqh_last = &TAILQ_NEXT((elm), field); \ } while (0) /* ---------- snd_midi (linkage members only) ---------- */ struct snd_midi { TAILQ_ENTRY(snd_midi) link; /* midi.c:118 */ int unit; }; static TAILQ_HEAD(, snd_midi) midi_devs; /* midi.c:169 */ static const char *g_mode; static void sigfault(int sig, siginfo_t *si, void *uc) { fprintf(stderr, "[%s] signal %d at %p -- unload loop dereferenced the " "value read from FREED memory (kernel: UAF read in " "TAILQ_FOREACH advance; lockmgr(&m->lock) on attacker-chosen " "pointer)\n", g_mode, sig, si->si_addr); exit(99); } /* midi.c:1436 kfree(m, M_MIDI) + simulated slab-zone reuse */ static void model_kfree(struct snd_midi *m) { free(m); if (strcmp(g_mode, "reuse") == 0) { /* same-size allocation immediately reoccupies the slot with * attacker-controlled contents (kernel: another M_MIDI * allocation, groomable by concurrent kmalloc churn) */ struct snd_midi *re = malloc(sizeof(*re)); memset(re, 0x41, sizeof(*re)); re->link.tqe_next = FAKE_NEXT; printf(" [groom] freed slot reoccupied; forged " "link.tqe_next = %p\n", (void *)FAKE_NEXT); fflush(stdout); } } /* midi.c:1418-1438 (essential steps verbatim) */ static int midi_destroy(struct snd_midi *m, int midiuninit) { (void)midiuninit; TAILQ_REMOVE(&midi_devs, m, link); model_kfree(m); return (0); } /* midi.c:1469-1477 (verbatim shape) */ static int midi_unload(void) { struct snd_midi *m; int retval; TAILQ_FOREACH(m, &midi_devs, link) { printf(" unload: visiting unit %d (%p)\n", m->unit, (void *)m); fflush(stdout); /* lockmgr(&m->lock, LK_EXCLUSIVE); if (m->busy) ... */ retval = midi_destroy(m, 1); if (retval) return (retval); /* loop advance: m = TAILQ_NEXT(m, link) <-- m is FREED */ } return (0); } /* fix.diff semantics: save the next pointer before destruction */ static int midi_unload_fixed(void) { struct snd_midi *m, *next; for (m = TAILQ_FIRST(&midi_devs); m != NULL; m = next) { next = TAILQ_NEXT(m, link); printf(" unload: visiting unit %d (%p)\n", m->unit, (void *)m); fflush(stdout); midi_destroy(m, 1); } return (0); } int main(int argc, char **argv) { struct snd_midi *d[3]; struct sigaction sa; int i; g_mode = argc > 1 ? argv[1] : "reuse"; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sigfault; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); TAILQ_INIT(&midi_devs); for (i = 0; i < 3; i++) { d[i] = calloc(1, sizeof(*d[i])); d[i]->unit = i; TAILQ_INSERT_TAIL(&midi_devs, d[i], link); } if (strcmp(g_mode, "fixed") == 0) { printf("[%s] fixed midi_unload(): stash next before destroy\n", g_mode); midi_unload_fixed(); printf("[%s] clean: all devices destroyed, no UAF\n", g_mode); return (0); } printf("[%s] buggy midi_unload(): TAILQ_FOREACH + free-in-body\n", g_mode); fflush(stdout); midi_unload(); printf("[%s] NOT REACHED: unload returned\n", g_mode); return (1); } |