/*
 * DF-5829 - vm_object_page_clean() partial-range silent no-op proof.
 *
 * The kernel marks pages PG_CLEANCHK for the cleaning pass only in
 * vm_object_page_clean_pass1(), and pass1 runs ONLY when the requested
 * range covers the whole object ("wholescan", i.e. start==0 and
 * end_pindex == object->size-1).  pass2 and vm_object_page_collect_flush()
 * both skip any page without PG_CLEANCHK, so ANY partial-range
 * vm_object_page_clean() - e.g. msync(MS_SYNC) of a subrange, or of a
 * whole mapping that is smaller than the file - flushes NOTHING and
 * still returns success.
 *
 * Observable: vm.stats.vm.v_vnodeout / v_vnodepgsout ("vnode pager
 * pageouts") are incremented in vnode_pager_putpages()
 * (sys/vm/vnode_pager.c:764), which is reached ONLY from
 * vm_pageout_flush() <- vm_object_page_collect_flush() <- pass2.
 * A real MS_SYNC flush MUST bump them; the no-op leaves them flat.
 *
 * Arms (each in a child that is SIGKILLed before teardown so exit
 * cannot flush anything):
 *   A partial   : 8-page file, mmap 8, dirty page 6, msync [5p,7p)  -> BUG: expect delta 0
 *   B wholefile : 8-page file, mmap 8, dirty page 6, msync [0p,8p)  -> ctrl: expect delta >=1
 *   C wholemap  : 8-page file, mmap 4, dirty page 1, msync [0p,4p)  -> BUG: expect delta 0
 *   D tinyfile  : 1-page file, mmap 1, dirty page 0, msync [0p,1p)  -> ctrl: expect delta >=1
 *
 * Build: cc -O2 -o msync_poc msync_poc.c
 * Run (unprivileged): ./msync_poc [reps]
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#define PGSZ 4096L
#define BASEDIR_D "/home/maxx/df5829"
static char path8[256], path1[256];

static unsigned long
ctr(const char *name)
{
	unsigned int v = 0;
	size_t l = sizeof(v);

	if (sysctlbyname(name, &v, &l, NULL, 0) < 0) {
		perror(name);
		exit(1);
	}
	return (v);
}

static void
makefile(const char *path, int npages)
{
	int fd;

	fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd < 0) { perror(path); exit(1); }
	if (ftruncate(fd, (off_t)npages * PGSZ)) { perror("ftruncate"); exit(1); }
	if (fsync(fd)) { perror("fsync"); exit(1); }
	close(fd);
}

/*
 * mmap map_pages of path, dirty exactly one page (dirty_idx), msync the
 * window [off_pages, off_pages+len_pages), print marker, then sleep so
 * the parent can sample counters while the mapping is still alive.
 */
static void
arm_child(const char *path, int map_pages, int dirty_idx,
	  long off_pages, long len_pages, int sleepsec)
{
	int fd, rc;
	char *p;

	fd = open(path, O_RDWR);
	if (fd < 0) { perror("open"); _exit(9); }
	p = mmap(NULL, map_pages * PGSZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (p == MAP_FAILED) { perror("mmap"); _exit(9); }
	memset(p + (long)dirty_idx * PGSZ, 0xAB, PGSZ);
	rc = msync(p + off_pages * PGSZ, len_pages * PGSZ, MS_SYNC);
	printf("    msync pages [%ld,%ld) rc=%d errno=%d\n",
	       off_pages, off_pages + len_pages, rc, errno);
	fflush(stdout);
	sleep(sleepsec);
	munmap(p, map_pages * PGSZ);
	close(fd);
	_exit(0);
}

static void
run_arm(const char *label, const char *path, int map_pages, int dirty_idx,
	long off_pages, long len_pages, unsigned long *d_out, unsigned long *d_pgs)
{
	unsigned long b1, b2, a1, a2;
	pid_t pid;

	sync();
	sleep(2);			/* quiesce syncer/bio noise */
	b1 = ctr("vm.stats.vm.v_vnodeout");
	b2 = ctr("vm.stats.vm.v_vnodepgsout");

	pid = fork();
	if (pid == 0)
		arm_child(path, map_pages, dirty_idx, off_pages, len_pages, 20);

	sleep(6);			/* msync of <=2 pages is instant; child sleeps */
	a1 = ctr("vm.stats.vm.v_vnodeout");
	a2 = ctr("vm.stats.vm.v_vnodepgsout");

	printf("%-34s vnodeout delta = %lu    vnodepgsout delta = %lu\n",
	       label, a1 - b1, a2 - b2);
	fflush(stdout);

	kill(pid, SIGKILL);		/* teardown must not flush */
	waitpid(pid, NULL, 0);

	*d_out = a1 - b1;
	*d_pgs = a2 - b2;
}

int
main(int argc, char **argv)
{
	const char *dir = (argc > 2) ? argv[2] : BASEDIR_D;
	int reps = (argc > 1) ? atoi(argv[1]) : 3;
	int infile_flushed, wholeobj_flushed;
	int r, fail = 0;
	unsigned long dA1, dA2, dB1, dB2, dC1, dC2, dD1, dD2, dE1, dE2, dF1, dF2;

	printf("DF-5829 msync partial-range no-op rig  (pid %d, uid %d)\n",
	       getpid(), getuid());
	printf("baseline vnodeout=%lu vnodepgsout=%lu\n\n",
	       ctr("vm.stats.vm.v_vnodeout"),
	       ctr("vm.stats.vm.v_vnodepgsout"));

	snprintf(path8, sizeof(path8), "%s/df5829_8p.bin", dir);
	snprintf(path1, sizeof(path1), "%s/df5829_1p.bin", dir);
	printf("test files under: %s\n\n", dir);
	makefile(path8, 8);
	makefile(path1, 1);

	for (r = 1; r <= reps; r++) {
		printf("== rep %d/%d ==\n", r, reps);
		run_arm("A partial-range  msync[5p,7p)",
			path8, 8, 6, 5, 2, &dA1, &dA2);
		run_arm("B whole-file ctrl msync[0p,8p)",
			path8, 8, 6, 0, 8, &dB1, &dB2);
		run_arm("C whole-mapping-of-8p-file msync[0p,4p)",
			path8, 4, 1, 0, 4, &dC1, &dC2);
		run_arm("D whole-file ctrl msync[0p,1p)",
			path1, 1, 0, 0, 1, &dD1, &dD2);
		/*
		 * E/F (block-rounded filesystems like hammer2 only):
		 * 1-page file, mmap/mysnc BEYOND EOF up to the block-rounded
		 * object size.  If the msync suddenly starts flushing, that
		 * proves the gate is exactly (end_pindex==object->size-1)
		 * with object->size rounded beyond EOF by nvnode_pager_setsize.
		 */
		run_arm("E 1p-file msync[0p,4p)-beyond-EOF",
			path1, 4, 0, 0, 4, &dE1, &dE2);
		run_arm("F 1p-file msync[0p,16p)-beyond-EOF",
			path1, 16, 0, 0, 16, &dF1, &dF2);

		printf("rep %d: A=%lu/%lu B=%lu/%lu C=%lu/%lu D=%lu/%lu "
		       "E=%lu/%lu F=%lu/%lu\n\n",
		       r, dA1, dA2, dB1, dB2, dC1, dC2, dD1, dD2,
		       dE1, dE2, dF1, dF2);

		if (dA1 != 0 || dA2 != 0)
			{ printf("!! rep %d arm A unexpectedly flushed\n", r); }
		if (dB1 == 0 && dB2 == 0)
			{ printf("   rep %d control B did not flush (expected on block-rounded fs)\n", r); }
		if (dC1 != 0 || dC2 != 0)
			{ printf("!! rep %d arm C unexpectedly flushed\n", r); }
		if (dD1 == 0 && dD2 == 0)
			{ printf("   rep %d control D did not flush (expected on block-rounded fs)\n", r); }
	}

	/*
	 * BUG CONFIRMED if no in-file-range msync (A,C and, on block-rounded
	 * filesystems, also B,D) performed any pageout, while a whole-object
	 * msync (B/D on UFS, or E/F beyond-EOF on hammer2) did.
	 */
	infile_flushed = (dA1|dA2|dC1|dC2) != 0;
	wholeobj_flushed = (dB1|dB2|dD1|dD2|dE1|dE2|dF1|dF2) != 0;
	if (!infile_flushed && wholeobj_flushed)
		printf("VERDICT: msync ranges that do not exactly cover "
		       "object->size performed ZERO pageout while the "
		       "whole-object msync did -> CONFIRMED\n");
	else if (infile_flushed)
		printf("VERDICT: partial range unexpectedly flushed -> "
		       "NOT REPRODUCED\n");
	else
		printf("VERDICT: no control flushed -> inconclusive\n");

	unlink(path8);
	unlink(path1);
	return (fail ? 1 : 0);
}
