/*
 * DF-3134 — dirfs_node_setpassive()'s check-then-close of dn_fd races
 * dirfs_findfd()'s ref-then-use: an in-flight consumer can operate on a
 * CLOSED (and host-RECYCLED) descriptor — metadata/data ops land on the
 * WRONG host file.
 *
 * dirfs is vkernel-only (sys/platform/vkernel64/conf/files), so — per the
 * DF-3054/DF-0806/DF-0807 precedent — this harness transcribes the exact
 * code path and orchestrates the interleaving deterministically with
 * step barriers (the race window in the real code is this exact
 * instruction sequence; nothing in dirfs synchronizes dn_fd):
 *
 *   dirfs_findfd (subr.c:450-497):
 *     :470   while (dnp1 == cur || dnp1->dn_fd == DIRFS_NOFD)   <- reads dn_fd
 *     :486       dirfs_node_ref(dnp1);       <- ref taken only AFTER the walk
 *              return dnp1;                  <- caller then uses dn_fd
 *
 *   consumer (e.g. dirfs_alloc_file :194/:202, vnops getattr :391/:393,
 *   nremove unlinkat :909, nmkdir :1051-1058, open_helper :602):
 *              openat(pathnp->dn_fd, ...)     <- reads dn_fd AGAIN, later
 *
 *   dirfs_node_setpassive(dmp, dnp, 0) (subr.c:825-867):
 *     :849       if (dirfs_node_refcnt(dnp) == 2 && vp && ... ) {
 *     :856           close(dnp->dn_fd);       <- check-then-close, no lock
 *     :857           dnp->dn_fd = DIRFS_NOFD;
 *
 * T1 walks and reads dn_fd=M;  T2 checks refcnt==2 (T1 has not ref'd yet)
 * and closes M;  T3 opens a decoy directory which recycles the SAME fd
 * number (lowest-free);  T1 refs, returns, and the consumer's
 * openat(M, "sentinel") / openat(M, "created", O_CREAT) execute against
 * the DECOY directory.  fstatat/openat on the recycled fd succeed and
 * read/create files that have nothing to do with the dirfs node.
 *
 * Variants:
 *   1) with recycler   -> WRONG-FILE operation confirmed (decoy sentinel
 *      read + file CREATED in the decoy directory through the stale fd)
 *   2) without recycler-> openat on the closed fd -> EBADF
 *      (use-after-close confirmed; in dirfs this surfaces as EBADF errors
 *      and, via alloc_file's openat==-1 path, the DF-0856 node leak)
 *   3) FIXED           -> dm_token (lwkt) held from before findfd's walk
 *      until dirfs_dropfd, and across setpassive's check+close: the close
 *      can no longer interleave; the consumer always hits the right file.
 *
 * exit code 2 == race demonstrated (wrong-file + EBADF) AND fixed variant
 * validated.  exit 1 == unexpected behavior.
 */
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define DIRFS_NOFD (-1)

struct dnode {
	int	 dn_fd;
	int	 dn_refcnt;
	struct dnode *dn_parent;
	const char *dn_name;
};

/* model mount */
static struct {
	pthread_mutex_t token;		/* FIXED-variant dm_token */
} dmp;

static struct dnode n_root, n_mid, n_tgt;

static atomic_int step, t1_locked;
static int use_fd;
static int recycler_present;
static const char *g_base;			/* consumer's captured dn_fd */
static int wrong_file_hits, ebadf_hits, fixed_ok;
static int g_wrong, g_ebadf, g_fixed;	/* latched per-variant results */

/* ---- verbatim-condition transcriptions --------------------------- */

/* dirfs_findfd walk (subr.c:470-481), path building elided (not needed
 * for the fd lifetime), returns the fd-holding ancestor like :483-496 */
static struct dnode *
findfd_walk(struct dnode *cur)
{
	struct dnode *dnp1 = cur;

	while (dnp1 == cur || dnp1->dn_fd == DIRFS_NOFD) {
		dnp1 = dnp1->dn_parent;		/* walk up (KKASSERT elided) */
	}
	return (dnp1);
}

/* dirfs_node_setpassive(dmp, dnp, 0) close section, subr.c:848-857:
 * refcnt==2 (vp-link + PASVFD), vp inactive, not root, not dirty. */
static void
setpassive0(struct dnode *dnp)
{
	if (dnp->dn_refcnt == 2 && dnp->dn_fd != DIRFS_NOFD) {
		close(dnp->dn_fd);		/* :856 */
		dnp->dn_fd = DIRFS_NOFD;	/* :857 */
	}
	dnp->dn_refcnt--;			/* dirfs_node_drop, :867 */
}

/* ------------------------------------------------------------------ */
static void *
t_findfd(void *arg)
{
	int fixed = (int)(intptr_t)arg;

	if (fixed) {
		pthread_mutex_lock(&dmp.token);
		atomic_store(&t1_locked, 1);
	}
	struct dnode *pathnp = findfd_walk(&n_tgt);	/* walk reads dn_fd */
	use_fd = pathnp->dn_fd;		/* consumer's read of pathnp->dn_fd */
	if (!fixed) {
		atomic_store(&step, 1);
		while (atomic_load(&step) < 3)
			sched_yield();
	}
	pathnp->dn_refcnt++;		/* dirfs_node_ref — subr.c:486 */

	/* consumer: e.g. getattr's fstatat / alloc_file's openat / nremove's
	 * unlinkat — all use pathnp->dn_fd after findfd returned */
	char buf[64];
	int sfd = openat(use_fd, "sentinel", O_RDONLY);
	if (sfd >= 0) {
		read(sfd, buf, sizeof(buf) - 1);
		close(sfd);
		if (strncmp(buf, "DECOY", 5) == 0)
			wrong_file_hits++;	/* read the DECOY's file */
		else if (strncmp(buf, "MID", 3) == 0)
			fixed_ok++;
	}
	int cfd = openat(use_fd, "created_by_race", O_CREAT | O_RDWR, 0644);
	if (cfd >= 0)
		close(cfd);
	else if (errno == EBADF)
		ebadf_hits++;
	if (fixed)
		pthread_mutex_unlock(&dmp.token);
	return (NULL);
}

static void *
t_setpassive(void *arg)
{
	int fixed = (int)(intptr_t)arg;

	if (!fixed)
		while (atomic_load(&step) < 1)
			sched_yield();
	if (fixed)
		while (atomic_load(&t1_locked) == 0)
			sched_yield();	/* deterministic: consumer first */
	if (fixed)
		pthread_mutex_lock(&dmp.token);
	setpassive0(&n_mid);			/* check-then-close :849-:857 */
	if (fixed)
		pthread_mutex_unlock(&dmp.token);
	atomic_store(&step, recycler_present ? 2 : 3);
	return (NULL);
}

static void *
t_recycle(void *arg)
{
	while (atomic_load(&step) < 2)
		sched_yield();
	char p[256];
	snprintf(p, sizeof(p), "%s/r/d", g_base);
	*((int *)arg) = open(p, O_DIRECTORY | O_RDONLY);	/* recycles fd */
	atomic_store(&step, 3);
	return (NULL);
}

/* ------------------------------------------------------------------ */
static int failures;

static void
check(int cond, const char *what)
{
	printf("    [%s] %s\n", cond ? "OK" : "!!", what);
	if (!cond)
		failures++;
}

static void
setup_tree(const char *base)
{
	char p[256];
	FILE *f;

	mkdir(base, 0755);
	snprintf(p, sizeof(p), "%s/r", base);	mkdir(p, 0755);
	snprintf(p, sizeof(p), "%s/r/m", base);	mkdir(p, 0755);
	snprintf(p, sizeof(p), "%s/r/d", base);	mkdir(p, 0755);
	snprintf(p, sizeof(p), "%s/r/m/sentinel", base);
	f = fopen(p, "w"); fputs("MID-sentinel-data", f); fclose(f);
	snprintf(p, sizeof(p), "%s/r/d/sentinel", base);
	f = fopen(p, "w"); fputs("DECOY-sentinel-data", f); fclose(f);
	snprintf(p, sizeof(p), "%s/r/m/created_by_race", base);
	unlink(p);
	snprintf(p, sizeof(p), "%s/r/d/created_by_race", base);
	unlink(p);
}

int
main(int argc, char **argv)
{
	const char *base = (argc > 1) ? argv[1] : "/tmp/df3134";
	char p[256];

	g_base = base;

	pthread_mutex_init(&dmp.token, NULL);
	printf("== DF-3134: setpassive check-then-close races findfd "
	    "ref-then-use (dn_fd unsynchronized) ==\n");

	/* ---------------- variant 1: with fd recycler ---------------- */
	printf("\n-- 1) close races in-flight use; fd number RECYCLED by a\n"
	    "      concurrent open (dirfs itself opens constantly)\n");
	setup_tree(base);
	{
		snprintf(p, sizeof(p), "%s/r/m", base);
		int rootfd = open(base, O_DIRECTORY | O_RDONLY);	/* low */
		int midfd  = open(p,   O_DIRECTORY | O_RDONLY);	/* next */
		n_root.dn_fd = rootfd; n_root.dn_name = "r";
		n_mid.dn_fd  = midfd;  n_mid.dn_name  = "m";
		n_mid.dn_refcnt = 2;		/* vp-link + PASVFD */
		n_tgt.dn_fd = DIRFS_NOFD;	n_tgt.dn_name = "t";
		n_tgt.dn_parent = &n_mid;	n_mid.dn_parent = &n_root;
		atomic_store(&step, 0);
		recycler_present = 1;

		pthread_t t1, t2, t3;
		int decoy_fd = -1;
		pthread_create(&t1, NULL, t_findfd, (void *)(intptr_t)0);
		pthread_create(&t2, NULL, t_setpassive, (void *)(intptr_t)0);
		pthread_create(&t3, NULL, t_recycle, &decoy_fd);
		pthread_join(t1, NULL); pthread_join(t2, NULL);
		pthread_join(t3, NULL);

		printf("    consumer used fd=%d; setpassive closed it; "
		    "recycler got fd=%d\n", use_fd, decoy_fd);
		check(decoy_fd == use_fd,
		    "BUG 1a: host recycled the closed fd number to an "
		    "unrelated open (lowest-free)");
		check(wrong_file_hits == 1,
		    "BUG 1b: consumer's openat(stale fd, 'sentinel') read "
		    "the DECOY directory's file — WRONG-FILE metadata op");
		snprintf(p, sizeof(p), "%s/r/d/created_by_race", base);
		check(access(p, F_OK) == 0,
		    "BUG 1c: consumer's O_CREAT created a file in the DECOY "
		    "directory — WRONG-FILE write-side op");
		snprintf(p, sizeof(p), "%s/r/m/created_by_race", base);
		check(access(p, F_OK) != 0,
		    "(the intended directory was NOT touched)");
		g_wrong = (wrong_file_hits > 0);
		close(rootfd); close(decoy_fd);
	}

	/* ---------------- variant 2: no recycler -------------------- */
	printf("\n-- 2) close races in-flight use; fd NOT recycled yet\n");
	setup_tree(base);
	{
		snprintf(p, sizeof(p), "%s/r/m", base);
		int rootfd = open(base, O_DIRECTORY | O_RDONLY);
		int midfd  = open(p,   O_DIRECTORY | O_RDONLY);
		n_root.dn_fd = rootfd;
		n_mid.dn_fd  = midfd;  n_mid.dn_refcnt = 2;
		n_tgt.dn_fd = DIRFS_NOFD; n_tgt.dn_parent = &n_mid;
		wrong_file_hits = ebadf_hits = 0;
		atomic_store(&step, 0);
		recycler_present = 0;

		/* same orchestration minus the recycler: step 2 published by
		 * t_setpassive directly unblocks the consumer */
		pthread_t t1, t2;
		pthread_create(&t1, NULL, t_findfd, (void *)(intptr_t)0);
		pthread_create(&t2, NULL, t_setpassive, (void *)(intptr_t)0);
		pthread_join(t1, NULL); pthread_join(t2, NULL);
		check(ebadf_hits == 1,
		    "BUG 2: openat on the closed fd -> EBADF (use-after-close; "
		    "in dirfs_alloc_file this also re-enters the DF-0856 "
		    "node-leak error path)");
		g_ebadf = (ebadf_hits > 0);
		close(rootfd);
	}

	/* ---------------- variant 3: FIXED (dm_token) --------------- */
	printf("\n-- 3) FIXED: dm_token serializes findfd..dropfd against\n"
	    "      setpassive's check+close\n");
	setup_tree(base);
	{
		snprintf(p, sizeof(p), "%s/r/m", base);
		int rootfd = open(base, O_DIRECTORY | O_RDONLY);
		int midfd  = open(p,   O_DIRECTORY | O_RDONLY);
		n_root.dn_fd = rootfd;
		n_mid.dn_fd  = midfd;  n_mid.dn_refcnt = 2;
		n_tgt.dn_fd = DIRFS_NOFD; n_tgt.dn_parent = &n_mid;
		wrong_file_hits = fixed_ok = 0;
		atomic_store(&step, 0);

		pthread_t t1, t2;
		/* token forces full serialization: whichever gets it first
		 * runs to completion — consumer completes before the close */
		pthread_create(&t1, NULL, t_findfd, (void *)(intptr_t)1);
		pthread_create(&t2, NULL, t_setpassive, (void *)(intptr_t)1);
		pthread_join(t1, NULL); pthread_join(t2, NULL);
		check(fixed_ok == 1 && wrong_file_hits == 0,
		    "FIXED: consumer read the CORRECT (mid) sentinel; no "
		    "wrong-file, no EBADF");
		snprintf(p, sizeof(p), "%s/r/m/created_by_race", base);
		check(access(p, F_OK) == 0,
		    "FIXED: creation landed in the intended directory");
		g_fixed = (fixed_ok > 0);
		close(rootfd);
	}

	printf("\n== summary: wrong-file=%d ebadf=%d fixed-ok=%d "
	    "mismatches=%d ==\n", g_wrong, g_ebadf, g_fixed, failures);
	if (failures == 0 && g_wrong && g_ebadf && g_fixed)
		return (2);	/* RACE CONFIRMED + FIX VALIDATED */
	return (failures ? 1 : 0);
}
