DF-3981 / sbp_replica.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 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | /* * sbp_replica.c - mechanism replica for the two sbp.c(4) pass-2 findings. * * MECHANISM A (DF-3981: login_callout UAF): * sbp.c arms sdev->login_callout (callout_reset, sbp.c:649) and then frees * sdev with kmalloc'd-container callout still armed on three paths * (sbp_alloc_lun stale-LUN sbp.c:444-445, !VALID_LUN loop sbp.c:536-539, * sbp_post_explore GC -> sbp_free_target:2002 -> sbp_free_sdev:1989) * without callout_stop(). This replica proves the DragonFly callout * subsystem fires the callback (and post-processes the external struct * callout, i.e. writes into the freed slab) after the container is freed: * * sysctl kern.sbpprep.a=1 -> kmalloc obj, arm 2*hz callout, kfree obj, * report; callback later kprintf's from the * freed object (arg == the freed pointer). * * MECHANISM B (DF-3982: blocking callout_stop from interrupt/crit context): * sbp_recv1 (hard-interrupt context: fwohci_intr -> fwohci_intr_body -> * fwohci_arcv(crit_enter, fwohci.c:2688) -> fw_rcv -> fw_xfer_done -> * sbp_recv(crit_enter, sbp.c:1823)) calls callout_stop() at sbp.c:1600, * 2590 (via sbp_dequeue_ocb) and 2705 (via sbp_abort_ocb). DragonFly's * current callout_stop() is synchronous (kern_timeout.c:1091 -> sync=1 -> * ssleep(..., "costp", ...) at kern_timeout.c:923) whenever the callback is * queued or INPROG on any CPU. This replica proves callout_stop() SLEEPS * ('costp') for as long as the callback remains in progress - including * when the caller holds a critical section, which is exactly the state * sbp_recv1 is in: * * sysctl kern.sbpprep.armb=1 -> arm 1-tick callout whose callback * sleeps 5s (mimic sbp_mgm_timeout -> * sbp_reset_start -> fw_asyreq work) * sysctl kern.sbpprep.stopb=1 -> from inside crit_enter(), wait for * callback INPROG, then callout_stop(); * kprintf timestamps around it. This * thread must disappear into 'costp' for * ~5s - sleeping inside a critical * section, which the hard-interrupt * sbp_recv1 path may not do. */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/malloc.h> #include <sys/callout.h> #include <sys/sysctl.h> #include <sys/systm.h> #include <sys/thread2.h> #include <sys/time.h> #include <sys/globaldata.h> /* mycpuid */ MALLOC_DEFINE(M_SBPPREP, "sbpprep", "sbp mechanism replica"); /* ---------------- Mechanism A: callout survives kfree ---------------- */ struct fake_sdev { char pad[64]; /* stand-in for sbp_dev head */ struct callout login_callout; /* sbp.c:183 */ char tail[64]; }; static struct fake_sdev *a_obj; static volatile int a_freed, a_fired; static void a_login_callout(void *arg) { /* * Real code (sbp_login_callout, sbp.c:625-629) immediately does * sbp_mgm_orb(sdev,...) on this pointer. We only *report* here so the * guest stays healthy: the point is that this fires at all after * kfree(). */ a_fired++; kprintf("sbpprep[A]: login_callout FIRED on %s object %p " "(arg == freed pointer: %s)\n", a_freed ? "FREED" : "live", arg, arg == (void *)a_obj ? "YES" : "?"); } static int a_sysctl(SYSCTL_HANDLER_ARGS) { int error, v = 0; error = sysctl_handle_int(oidp, &v, 0, req); if (error || req->newptr == NULL) return error; if (v == 1) { a_fired = a_freed = 0; a_obj = kmalloc(sizeof(*a_obj), M_SBPPREP, M_WAITOK | M_ZERO); callout_init(&a_obj->login_callout); callout_reset(&a_obj->login_callout, 2 * hz, a_login_callout, a_obj); kprintf("sbpprep[A]: armed login_callout in %p, tick=%d\n", a_obj, ticks); /* * sbp_free_sdev (sbp.c:1978-1990) has no callout_stop(): * destroy the DMA map, free the dma, kfree(sdev) - callout * left armed. */ kfree(a_obj, M_SBPPREP); a_freed = 1; kprintf("sbpprep[A]: kfree(%p) DONE - callout still armed, " "no callout_stop() (sbp.c:1978 has none)\n", a_obj); } return 0; } /* ------------- Mechanism B: blocking callout_stop in crit ------------- */ static struct callout b_c; /* mgm_ocb_timeout replica */ static volatile int b_started, b_done; static int b_stop_elapsed_ms; static volatile int b_in_stop; static struct sysctl_ctx_list sbpprep_sysctl_ctx; static void b_mgm_timeout(void *arg) { b_started++; /* * sbp_mgm_timeout (sbp.c:2126-2146) itself does kprintf + * sbp_reset_start -> sbp_write_cmd -> fw_asyreq, whose completion * depends on bus answers (tlabel allocation, retries) - i.e. an * unbounded body. Stand in a body that completes only when the * controller (kern.sbpprep_wakeb) releases it. */ kprintf("sbpprep[B]: mgm_timeout replica callback STARTED " "(INPROG set, cpu=%d)\n", mycpuid); tsleep(&b_done, 0, "bcb", 0); /* no timeout: woken by wakeb */ b_done++; kprintf("sbpprep[B]: mgm_timeout replica callback DONE " "(INPROG cleared)\n"); } static int armb_sysctl(SYSCTL_HANDLER_ARGS) { int error, v = 0; error = sysctl_handle_int(oidp, &v, 0, req); if (error || req->newptr == NULL) return error; if (v == 1) { b_started = b_done = 0; callout_reset(&b_c, 1, b_mgm_timeout, NULL); kprintf("sbpprep[B]: armed 1-tick mgm_ocb_timeout replica\n"); } return 0; } static int stopb_sysctl(SYSCTL_HANDLER_ARGS) { int error, v = 0; struct timeval t0, t1; error = sysctl_handle_int(oidp, &v, 0, req); if (error || req->newptr == NULL) return error; if (v != 1) return 0; /* * Replicate the sbp_recv1 state: hard-interrupt path runs under * crit_enter() (fwohci.c:2688 + sbp.c:1823). The callback is * already INPROG (armed ~2s ago, 6s body, and it busy-waits so it * cannot finish early), then callout_stop() under the critical * section - exactly sbp.c:1600. */ crit_enter(); if (!b_started) { crit_exit(); kprintf("sbpprep[B]: ERROR - callback not started yet\n"); return 0; } b_in_stop = 1; microtime(&t0); kprintf("sbpprep[B]: callout_stop() ENTERED inside crit section " "(sbp.c:1600 state) at %ld.%06ld\n", (long)t0.tv_sec, t0.tv_usec); callout_stop(&b_c); microtime(&t1); b_stop_elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000 + (t1.tv_usec - t0.tv_usec) / 1000; b_in_stop = 0; kprintf("sbpprep[B]: callout_stop() RETURNED after %d ms at " "%ld.%06ld\n", b_stop_elapsed_ms, (long)t1.tv_sec, t1.tv_usec); crit_exit(); kprintf("sbpprep[B]: VERDICT: callout_stop %s while INPROG - " "caller slept in 'costp' inside a critical section\n", b_stop_elapsed_ms >= 3000 ? "BLOCKED" : "did not block"); return 0; } static int wakeb_sysctl(SYSCTL_HANDLER_ARGS) { int error, v = 0; error = sysctl_handle_int(oidp, &v, 0, req); if (error || req->newptr == NULL) return error; if (v != 1) return 0; /* * Simulated bus answer releasing the mgm callback after >=3s of the * caller being stuck in 'costp'. */ kprintf("sbpprep[B]: wakeb: releasing callback in 3s " "(stopper has been in 'costp' all this time)\n"); tsleep(&b_done, 0, "wkdelay", 3 * hz); wakeup(&b_done); kprintf("sbpprep[B]: wakeb: wakeup(&b_done) issued\n"); return 0; } static int sbpprep_modevent(module_t mod, int type, void *data) { switch (type) { case MOD_LOAD: callout_init(&b_c); sysctl_ctx_init(&sbpprep_sysctl_ctx); SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx, SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO, "sbpprep_a", CTLTYPE_INT|CTLFLAG_RW, 0, 0, a_sysctl, "I", "A: arm+free callout container (DF-3981 mechanism)"); SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx, SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO, "sbpprep_armb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, armb_sysctl, "I", "B: arm mgm-timeout replica (DF-3982 mechanism)"); SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx, SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO, "sbpprep_stopb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, stopb_sysctl, "I", "B: callout_stop under crit while INPROG"); SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx, SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO, "sbpprep_wakeb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, wakeb_sysctl, "I", "B: release the blocked callback"); kprintf("sbpprep: loaded\n"); break; case MOD_UNLOAD: callout_stop(&b_c); sysctl_ctx_free(&sbpprep_sysctl_ctx); kprintf("sbpprep: unloaded\n"); break; default: return EOPNOTSUPP; } return 0; } static moduledata_t sbpprep_mod = { "sbpprep", sbpprep_modevent, 0 }; DECLARE_MODULE(sbpprep, sbpprep_mod, SI_SUB_DRIVERS, SI_ORDER_ANY); |