/*
 * DF-3133 — dirfs_node_chsize() mutates the vnode buffer/pager state BEFORE
 * the host truncate() is known to succeed (and never checks the NULL return
 * of dirfs_node_absolute_path()).
 *
 * dirfs is vkernel-only (sys/platform/vkernel64/conf/files), so — per the
 * DF-3054/DF-0806/DF-0807 precedent — this harness is a faithful
 * transcription of the exact code path:
 *
 *   - dirfs_node_absolute_path()  : verbatim from dirfs_subr.c:377-443
 *   - dirfs_node_chsize()         : verbatim control flow from
 *                                   dirfs_subr.c:748-799, with the two
 *                                   buffer-cache helpers stubbed to mirror
 *                                   sys/kern/vfs_vm.c semantics:
 *     nvtruncbuf  (vfs_vm.c:129-261): destroys ALL buffers (clean AND dirty)
 *                   at/beyond the truncation point WITHOUT flushing them
 *                   (nvtruncbuf_bp_trunc: bremfree + B_INVAL|B_RELBUF|
 *                   B_NOCACHE + brelse, vfs_vm.c:300-302) and then sets the
 *                   pager size (nvnode_pager_setsize).
 *     nvextendbuf (vfs_vm.c:376+):   instantiates zero-filled buffers for
 *                   [olength, nlength) and sets the pager size.
 *   - buffer read/write: mirrors dirfs_read (vnops:618-652, buffer cache
 *                   first) and dirfs_strategy (pread/pwrite on the still
 *                   open dn_fd), write(2) => bdwrite => dirty, unflushed.
 *
 * Scenarios (all as the invoking unprivileged user, own files only —
 * except (C) which only *attempts* a truncate of a root-owned file and
 * verifies afterwards that it is byte-identical):
 *
 *  A  unlinked-but-open file: absolute_path()==NULL -> truncate(NULL) EFAULT,
 *     but nvtruncbuf already destroyed the dirty buffer holding an
 *     acknowledged (write(2)-returned) write, and the pager.  Host file data
 *     is still alive through the open descriptor (POSIX unlink semantics),
 *     and re-reads resurrect the OLD data: the acknowledged write is
 *     silently reverted.
 *  B  over-length absolute path (>MAXPATHLEN), constructed for real on the
 *     host via fd-relative openat() — exactly how dirfs itself builds such
 *     trees through its passive fd cache.  Same damage, EFAULT again.
 *     Boundary sub-case: a just-fits path truncates normally (sanity).
 *  C  valid path, host refuses the truncate (root-owned file -> EPERM):
 *     pager/buffers destroyed even though nothing changed on the host.
 *  D  FIXED variant of chsize (NULL check + host-truncate-first): zero
 *     damage in every failure scenario, error propagated.
 *
 * exit code 2  == BUG CONFIRMED (all failure scenarios show damage) AND
 *                 FIXED variant validated (zero damage).
 * exit code 1  == something did not behave as predicted (investigate).
 */
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define MAXPATHLEN 1024
#define BSIZE      16384
#define NBLKS      4
#define DIRFS_NOFD (-1)

/* ------------------------------------------------------------------ */
/* dirfs node model (subset)                                           */
/* ------------------------------------------------------------------ */
struct dnode {
	const char	*name;		/* dn_name */
	int		 namelen;		/* dn_namelen */
	struct dnode	*parent;	/* dn_parent; NULL also models the
					 * post-dirfs_nremove state
					 * (vnops.c:914-916) */
	int		 state;
	int		 fd;			/* dn_fd (host descriptor) */
	off_t		 size;		/* dn_size */
	/* guest buffer cache for this vnode */
	struct {
		int	 valid;
		int	 dirty;		/* B_DELWRI, write(2) acked, unflushed */
		char	 data[BSIZE];
	} blk[NBLKS];
	off_t		 pager_size;	/* VM object size (nvnode_pager_setsize) */
	int		 dirty_discarded; /* B_INVAL destroyed dirty buffers */
	int		 clean_discarded;
};

#define DIRFS_ROOT 0x00000001
#define dirfs_node_isroot(n) ((n)->state & DIRFS_ROOT)

static char dm_path[MAXPATHLEN + 1];	/* dmp->dm_path */

static char *sprintf_ret(char *, size_t, const char *, const char *);
static int nvtruncbuf_stub_call_trunc(struct dnode *, off_t, int);
static int nvextendbuf_stub_call_ext(struct dnode *, off_t, off_t, int, int);

/* ------------------------------------------------------------------ */
/* dirfs_node_absolute_path — VERBATIM transcription of subr.c:377-443  */
/* (only the node field names are the model's)                          */
/* ------------------------------------------------------------------ */
static char *
dirfs_node_absolute_path(struct dnode *cur, char **pathfreep)
{
	size_t len;
	struct dnode *dnp1;
	char *buf;
	int count;

	*pathfreep = NULL;
	if (cur == NULL)
		return NULL;
	buf = malloc(MAXPATHLEN + 1);

	/*
	 * Iterate through the parents until we hit the root.
	 */
	count = 0;
	buf[MAXPATHLEN] = 0;
	dnp1 = cur;
	while (dirfs_node_isroot(dnp1) == 0) {
		count += dnp1->namelen;
		if (count <= MAXPATHLEN)
			memcpy(&buf[MAXPATHLEN - count], dnp1->name,
			       dnp1->namelen);
		++count;
		if (count <= MAXPATHLEN)
			buf[MAXPATHLEN - count] = '/';
		dnp1 = dnp1->parent;
		if (dnp1 == NULL)
			break;			/* unlinked: no path (subr.c:423) */
	}

	/*
	 * Prefix with the root mount path.
	 */
	len = strlen(dm_path);
	count += len;
	if (dnp1 && count <= MAXPATHLEN) {
		memcpy(&buf[MAXPATHLEN - count], dm_path, len);
		*pathfreep = buf;
		return (&buf[MAXPATHLEN - count]);
	} else {
		free(buf);
		*pathfreep = NULL;
		return (NULL);
	}
}

/* ------------------------------------------------------------------ */
/* nvtruncbuf stub — mirrors sys/kern/vfs_vm.c:129-261                  */
/*   destroys buffers at/beyond the (block-rounded) truncation point,   */
/*   dirty ones are NOT flushed (B_INVAL|B_NOCACHE), pager size set.    */
/* ------------------------------------------------------------------ */
static void
nvtruncbuf(struct dnode *dnp, off_t length)
{
	int truncblk = (int)((length + BSIZE - 1) / BSIZE);	/* first dead blk */

	for (int i = truncblk; i < NBLKS; i++) {
		if (!dnp->blk[i].valid)
			continue;
		if (dnp->blk[i].dirty) {
			dnp->dirty_discarded++;
			/* B_INVAL: contents destroyed, host never sees them */
		} else {
			dnp->clean_discarded++;
		}
		memset(&dnp->blk[i], 0, sizeof(dnp->blk[i]));
	}
	dnp->pager_size = length;			/* nvnode_pager_setsize */
}

/* nvextendbuf stub — vfs_vm.c:376+: zero-fill [olength,nlength), set size */
static void
nvextendbuf(struct dnode *dnp, off_t olength, off_t nlength)
{
	int oblk = (int)((olength + BSIZE - 1) / BSIZE);
	int nblk = (int)((nlength + BSIZE - 1) / BSIZE);

	for (int i = oblk; i < nblk && i < NBLKS; i++) {
		if (!dnp->blk[i].valid) {
			dnp->blk[i].valid = 1;
			dnp->blk[i].dirty = 0;
			memset(dnp->blk[i].data, 0, BSIZE);
		}
	}
	dnp->pager_size = nlength;
}

/* ------------------------------------------------------------------ */
/* guest I/O simulation: dirfs_read (:618-652)/write + strategy pread   */
/* ------------------------------------------------------------------ */
static void
guest_write_blk(struct dnode *dnp, int blkno, const char *tag)
{
	dnp->blk[blkno].valid = 1;
	dnp->blk[blkno].dirty = 1;			/* write(2) acked, bdwrite */
	snprintf(dnp->blk[blkno].data, BSIZE, "%s", tag);
	if ((off_t)(blkno + 1) * BSIZE > dnp->size)
		dnp->size = (off_t)(blkno + 1) * BSIZE; /* dirfs_write:719-722 */
}

static const char *
guest_read_blk(struct dnode *dnp, int blkno)
{
	static char tmp[BSIZE];

	if (dnp->blk[blkno].valid)			/* buffer cache hit */
		return (dnp->blk[blkno].data);
	/* miss -> dirfs_strategy: pread(dnp->dn_fd) from the HOST */
	ssize_t n = pread(dnp->fd, tmp, BSIZE, (off_t)blkno * BSIZE);
	if (n <= 0) {
		/* strategy zero-fills short/failed reads (vnops:828-829) */
		memset(tmp, 0, BSIZE);
		snprintf(tmp, BSIZE, "<zero-fill:%d>", (int)n);
	} else {
		tmp[n] = 0;
	}
	return (tmp);
}

/* ------------------------------------------------------------------ */
/* dirfs_node_chsize — VERBATIM control flow of subr.c:748-799          */
/* ------------------------------------------------------------------ */
static int
dirfs_node_chsize(struct dnode *dnp, off_t nsize)
{
	int error = 0;
	char *tmp, *pathfree;
	off_t osize = dnp->size;
	int biosize = BSIZE;

	/* v_type == VREG is a precondition here */

	tmp = dirfs_node_absolute_path(dnp, &pathfree);		/* :778 */
	if (nsize < osize) {					/* :779-780 */
		error = nvtruncbuf_stub_call_trunc(dnp, nsize, biosize);
	} else {						/* :781-784 */
		error = nvextendbuf_stub_call_ext(dnp, osize, nsize,
		    biosize, biosize);
	}
	if (error == 0 && truncate(tmp, nsize) < 0)		/* :786-787 */
		error = errno;
	if (error == 0)					/* :788-789 */
		dnp->size = nsize;

	free(pathfree);					/* dirfs_dropfd */
	return (error);
}
/* small shims so the verbatim body above stays recognizable */
static int nvtruncbuf_stub_call_trunc(struct dnode *d, off_t n, int b)
{ nvtruncbuf(d, n); (void)b; return (0); }
static int nvextendbuf_stub_call_ext(struct dnode *d, off_t o, off_t n,
    int b1, int b2)
{ nvextendbuf(d, o, n); (void)b1; (void)b2; return (0); }

/* ------------------------------------------------------------------ */
/* FIXED chsize: NULL check + host-first, buffer state only on success  */
/* ------------------------------------------------------------------ */
static int
dirfs_node_chsize_fixed(struct dnode *dnp, off_t nsize)
{
	int error = 0;
	char *tmp, *pathfree;
	off_t osize = dnp->size;

	tmp = dirfs_node_absolute_path(dnp, &pathfree);
	if (tmp == NULL) {
		error = ENAMETOOLONG;		/* unlinked / over-length */
		goto out;
	}
	if (truncate(tmp, nsize) < 0) {		/* host FIRST: it may fail */
		error = errno;
		goto out;
	}
	if (nsize < osize)
		nvtruncbuf(dnp, nsize);
	else
		nvextendbuf(dnp, osize, nsize);
	dnp->size = nsize;
out:
	free(pathfree);
	return (error);
}

/* ------------------------------------------------------------------ */
static int failures, confirmed;

static void
check(int cond, const char *what)
{
	printf("    [%s] %s\n", cond ? "OK" : "!!", what);
	if (!cond)
		failures++;
	else if (strncmp(what, "BUG", 3) == 0)
		confirmed++;
}

static void
fill_host_file(int fd, const char *tag)
{
	char b[BSIZE];
	memset(b, 'H', BSIZE);
	for (int i = 0; i < NBLKS; i++) {
		snprintf(b, BSIZE, "%s-blk%d", tag, i);
		b[strlen(b)] = 'H';
		pwrite(fd, b, BSIZE, (off_t)i * BSIZE);
	}
}

static void
host_read_blk(int fd, int blkno, char *out, size_t outlen)
{
	ssize_t n = pread(fd, out, outlen - 1, (off_t)blkno * BSIZE);
	if (n <= 0)
		snprintf(out, outlen, "<host-read-%d>", (int)n);
	else {
		out[n < (ssize_t)outlen - 1 ? n : outlen - 1] = 0;
		for (size_t i = 0; i < strlen(out); i++)
			if (out[i] == 'H') { out[i] = 0; break; }
	}
}

static struct dnode mknodes(const char **names, int n, int fd)
{
	static struct dnode root;	/* static: dn_parent chain target */
	static struct dnode chain[8];

	memset(&root, 0, sizeof(root));
	root.state = DIRFS_ROOT;
	root.fd = DIRFS_NOFD;
	struct dnode *cur = &root;
	memset(chain, 0, sizeof(chain));
	memset(chain, 0, sizeof(chain));
	for (int i = 0; i < n; i++) {
		chain[i].name = names[i];
		chain[i].namelen = strlen(names[i]);
		chain[i].parent = cur;
		chain[i].fd = DIRFS_NOFD;
		cur = &chain[i];
	}
	chain[n - 1].fd = fd;
	chain[n - 1].size = (off_t)NBLKS * BSIZE;
	chain[n - 1].pager_size = (off_t)NBLKS * BSIZE;
	for (int i = 0; i < NBLKS; i++) {
		chain[n - 1].blk[i].valid = 1;	/* clean cached blocks */
		chain[n - 1].blk[i].dirty = 0;
	}
	return (chain[n - 1]);
}

int
main(int argc, char **argv)
{
	char base[256], path[512];
	const char *root;
	int fd;
	char buf[BSIZE];

	root = (argc > 1) ? argv[1] : "/tmp/df3133";
	snprintf(base, sizeof(base), "%s", root);
	mkdir(base, 0755);

	printf("== DF-3133: dirfs_node_chsize destructive-before-validate ==\n");

	/* ---------------- Scenario A: unlinked-but-open -------------- */
	printf("\n-- A) unlinked-but-open file (dn_parent NULLed by "
	    "dirfs_nremove vnops.c:914-916)\n");
	mkdir(sprintf_ret(path, sizeof(path), base, "/a"), 0755) == 0 ||
	    errno == EEXIST;
	snprintf(dm_path, sizeof(dm_path), "%s/a", base);
	strcat(path, "/f");
	unlink(path);
	fd = open(path, O_RDWR | O_CREAT, 0644);
	fill_host_file(fd, "OLDDATA");
	{
		struct dnode f = mknodes((const char *[]){"f"}, 1, fd);
		/* write(2) acknowledged at blk 3, still dirty (bdwrite) */
		guest_write_blk(&f, 3, "NEWDATA-acked-write");

		/* dirfs_nremove succeeded on the host */
		if (unlink(path) != 0) { perror("unlink"); return (1); }
		f.parent = NULL;		/* vnops.c:914-916 */

		int err = dirfs_node_chsize(&f, 0);
		printf("    chsize returned %d (%s)\n", err, strerror(err));
		check(err == EFAULT, "BUG A1: truncate(NULL) -> EFAULT "
		    "after damage already done (subr.c:778-787)");
		check(f.dirty_discarded == 1,
		    "BUG A2: acknowledged (dirty) write DESTROYED without "
		    "flush (vfs_vm.c:300-302 via nvtruncbuf)");
		check(f.pager_size == 0,
		    "BUG A3: pager/VM object truncated to 0 while host file "
		    "unchanged (mmap view SIGBUS)");
		host_read_blk(fd, 3, buf, sizeof(buf));
		check(strcmp(buf, "OLDDATA-blk3") == 0,
		    "host data still alive via open fd (POSIX unlink "
		    "semantics intact on host)");
		const char *g = guest_read_blk(&f, 3);
		check(strncmp(g, "OLDDATA-blk3", 12) == 0,
		    "BUG A4: guest re-read resurrects OLD data — the "
		    "acknowledged write is silently REVERTED");

		/* FIXED variant, same state */
		struct dnode f2 = mknodes((const char *[]){"f"}, 1, fd);
		guest_write_blk(&f2, 3, "NEWDATA-acked-write");
		f2.parent = NULL;
		int err2 = dirfs_node_chsize_fixed(&f2, 0);
		check(err2 == ENAMETOOLONG, "FIXED A: clean ENAMETOOLONG");
		check(f2.dirty_discarded == 0 && f2.pager_size > 0,
		    "FIXED A: zero damage, dirty buffer intact");
	}
	close(fd);

	/* ---------------- Scenario B: over-length path ---------------- */
	printf("\n-- B) absolute path > MAXPATHLEN built via fd-relative "
	    "openat() (the dirfs fd-cache trick)\n");
	mkdir(sprintf_ret(path, sizeof(path), base, "/b"), 0755) == 0 ||
	    errno == EEXIST;
	snprintf(dm_path, sizeof(dm_path), "%s/b", base);
	{
		/* 4x240 fits: 4*241 + strlen(dm_path) + 2 <= 1024 */
		char nm[256];
		int lvl, dirfd = open(dm_path, O_DIRECTORY | O_RDONLY);
		for (lvl = 0; lvl < 4; lvl++) {
			memset(nm, 'a' + lvl, 240); nm[240] = 0;
			if (mkdirat(dirfd, nm, 0755) != 0 && errno != EEXIST) {
				perror("mkdirat"); return (1);
			}
			int nfd = openat(dirfd, nm, O_DIRECTORY | O_RDONLY);
			if (nfd < 0) { perror("openat"); return (1); }
			close(dirfd);
			dirfd = nfd;
		}
		/* boundary: file fits (count <= MAXPATHLEN) — sanity */
		int ffd = openat(dirfd, "f", O_RDWR | O_CREAT, 0644);
		fill_host_file(ffd, "FIT");
		const char *names5[5];
		static char keep[5][256];
		for (int i = 0; i < 4; i++) {
			memset(keep[i], 'a' + i, 240); keep[i][240] = 0;
			names5[i] = keep[i];
		}
		names5[4] = "f";
		struct dnode bf = mknodes(names5, 5, ffd);
		int berr = dirfs_node_chsize(&bf, 0);
		check(berr == 0 && bf.dirty_discarded == 0,
		    "B-sanity: 4x240 boundary path truncates normally");
		close(ffd);

		/* 5th level: mkdirat/openat succeed RELATIVE (host allows),
		 * but the absolute path now exceeds MAXPATHLEN */
		memset(keep[4], 'e', 240); keep[4][240] = 0;
		names5[4] = keep[4];
		if (mkdirat(dirfd, keep[4], 0755) != 0 && errno != EEXIST) {
			perror("mkdirat5"); return (1);
		}
		int d5 = openat(dirfd, keep[4], O_DIRECTORY | O_RDONLY);
		int ffd5 = openat(d5, "f", O_RDWR | O_CREAT, 0644);
		fill_host_file(ffd5, "DEEP");
		check(ffd5 >= 0, "BUG precondition: >MAXPATHLEN tree IS "
		    "constructible via fd-relative openat() (dirfs does the "
		    "same through the passive fd cache)");
		close(d5);
		close(dirfd);

		const char *names6[6] = { keep[0], keep[1], keep[2], keep[3],
					  keep[4], "f" };
		struct dnode df = mknodes(names6, 6, ffd5);
		guest_write_blk(&df, 1, "DEEP-acked-write");
		int derr = dirfs_node_chsize(&df, 0);
		printf("    chsize returned %d (%s)\n", derr, strerror(derr));
		check(derr == EFAULT, "BUG B1: absolute_path NULL (count > "
		    "MAXPATHLEN, subr.c:433-442) -> truncate(NULL) EFAULT");
		check(df.dirty_discarded == 1 && df.pager_size == 0,
		    "BUG B2: dirty buffer destroyed + pager truncated "
		    "before the failing truncate");

		struct dnode df2 = mknodes(names6, 6, ffd5);
		guest_write_blk(&df2, 1, "DEEP-acked-write");
		int derr2 = dirfs_node_chsize_fixed(&df2, 0);
		check(derr2 == ENAMETOOLONG && df2.dirty_discarded == 0,
		    "FIXED B: ENAMETOOLONG, zero damage");
		close(ffd5);
	}

	/* ---------------- Scenario C: host EPERM, valid path ----------- */
	printf("\n-- C) valid path, host refuses truncate (root-owned file) "
	    "-> EPERM after damage\n");
	{
		const char *victim = "/etc/hostid";
		struct stat st;
		if (stat(victim, &st) != 0 || st.st_size == 0 ||
		    st.st_uid == getuid()) {
			victim = "/etc/rc";
			if (stat(victim, &st) != 0 || st.st_size == 0 ||
			    st.st_uid == getuid()) {
				printf("    (skipped: no suitable root-owned "
				    "file)\n");
				goto done_C;
			}
		}
		int vfd = open(victim, O_RDONLY);
		snprintf(dm_path, sizeof(dm_path), "%s/etc", "/");
		/* model node "hostid" under dm_path=/etc */
		struct dnode vn = mknodes((const char *[]){
		    st.st_uid == getuid() ? "x" : "hostid"}, 1, vfd);
		const char *bname = strrchr(victim, '/') + 1;
		vn.name = bname;
		vn.namelen = strlen(bname);
		vn.size = st.st_size;
		vn.pager_size = st.st_size;
		guest_write_blk(&vn, 0, "GUEST-acked-write");
		off_t before = st.st_size;

		int cerr = dirfs_node_chsize(&vn, 0);
		printf("    chsize returned %d (%s)\n", cerr, strerror(cerr));
		check(cerr == EPERM || cerr == EACCES,
		    "BUG C1: host refuses the truncate (non-owner)");
		check(vn.dirty_discarded == 1 && vn.pager_size == 0,
		    "BUG C2: guest pager/buffers for a file we cannot modify "
		    "were destroyed anyway (subr.c:779-785 BEFORE :786)");
		struct stat st2;
		stat(victim, &st2);
		check(st2.st_size == before && st2.st_mtime == st.st_mtime,
		    "C: host file untouched (verified byte-identical state)");
		close(vfd);
	}
done_C:

	printf("\n== summary: %d bug-confirmations, %d mismatches ==\n",
	    confirmed, failures);
	if (failures == 0 && confirmed >= 6)
		return (2);	/* BUG CONFIRMED + FIX VALIDATED */
	return (failures ? 1 : 0);
}

static char *
sprintf_ret(char *s, size_t n, const char *a, const char *b)
{
	snprintf(s, n, "%s%s", a, b);
	return (s);
}
