/*
 * DF-2920 proof-of-concept: beyond-EOF mmap fault behavior after a
 * mid-block ftruncate() on DragonFlyBSD.
 *
 * The nv* VFS/VM coherency layer (sys/kern/vfs_vm.c) claims that pages
 * beyond file EOF which still fit inside the last buffer cache buffer are
 * "unmapped and userland is not allowed to fault them in"
 * (sys/kern/vfs_vm.c:46-47).  nvnode_pager_setsize() only does a one-shot
 * pmap unmap (vfs_vm.c:487-495); the pages stay fully valid in the VM
 * object, and vm_fault_object() (sys/vm/vm_fault.c:1906-1968) maps any
 * present+valid page with NO v_filesize check, so a fresh fault re-maps
 * them.
 *
 * Subtests:
 *   S0  sanity: pread() beyond EOF returns 0 (EOF enforced for read)
 *   S1  fresh mmap AFTER truncate: read @ within-last-buffer page
 *       (expect SIGBUS per design; predict: data) vs far page
 *       (beyond object size; expect SIGBUS).
 *   S2  pre-existing mapping across truncate: re-read beyond EOF.
 *   S3  fresh mmap WRITE beyond EOF; extend back; pread() to see if the
 *       written byte became file content.
 *   S4  race: reader thread hammers beyond-EOF byte while truncator
 *       cycles truncate/refill; counts pattern sightings while the
 *       truncator-side size flag says "small" (i.e. during/after
 *       ftruncate-down) -- demonstrates the zero-fill window exposure.
 */
#include <sys/param.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define FILESZ  (5*65536)          /* 327680 */
#define NEWSIZE (65536 + 1231)     /* 66767: mid-block for any 4K..64K pow2 blksize */
#define PROBE1  70000              /* beyond EOF, inside last buffer (page present) */
#define PROBE2  200000             /* beyond EOF, beyond object size (page absent) */
#define PATTERN 0x5A

static sigjmp_buf sjb;
static volatile sig_atomic_t in_probe;

static void
sigbus_handler(int sig __unused)
{
	if (in_probe)
		siglongjmp(sjb, 1);
	_exit(99); /* fatal fault outside a probe: die loudly */
}

static void
install_handlers(void)
{
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = sigbus_handler;
	sigemptyset(&sa.sa_mask);
	sigaction(SIGBUS, &sa, NULL);
	sigaction(SIGSEGV, &sa, NULL);
}

static int fd = -1;
static char path[512];

static void
fill_file(unsigned char v)
{
	static unsigned char buf[65536];
	ssize_t n;
	off_t off = 0;

	memset(buf, v, sizeof(buf));
	while (off < FILESZ) {
		size_t len = (off_t)FILESZ - off > (off_t)sizeof(buf) ?
		    sizeof(buf) : (size_t)((off_t)FILESZ - off);
		n = pwrite(fd, buf, len, off);
		if (n != (ssize_t)len)
			err(1, "pwrite fill");
		off += n;
	}
	if (fsync(fd))
		err(1, "fsync");
}

/* returns -1 on SIGBUS */
static int
probe_read(volatile unsigned char *p)
{
	int r;
	in_probe = 1;
	r = sigsetjmp(sjb, 1);
	if (r == 0) {
		unsigned char v = *p;
		in_probe = 0;
		return v;
	}
	in_probe = 0;
	return -1;
}

static int
probe_write(volatile unsigned char *p, unsigned char v)
{
	int r;
	in_probe = 1;
	r = sigsetjmp(sjb, 1);
	if (r == 0) {
		*p = v;
		in_probe = 0;
		return 0;
	}
	in_probe = 0;
	return -1;
}

static void
report(const char *what, int v)
{
	if (v < 0)
		printf("  %-42s : SIGBUS\n", what);
	else
		printf("  %-42s : value 0x%02x (%s)\n", what, v,
		    v == PATTERN ? "PRE-TRUNCATE PATTERN!" :
		    v == 0x00 ? "zero" : "other");
}

static void
subtest_s0(void)
{
	char c;
	printf("S0: pread() beyond EOF\n");
	fill_file(PATTERN);
	if (ftruncate(fd, NEWSIZE))
		err(1, "S0 ftruncate");
	errno = 0;
	ssize_t n = pread(fd, &c, 1, PROBE1);
	printf("  pread @%d returned %zd (errno %d) -- %s\n", PROBE1, n,
	    errno, n == 0 ? "EOF enforced for read()" : "DATA LEAK VIA READ");
}

static void
subtest_s1(void)
{
	volatile unsigned char *map;
	printf("S1: fresh mmap created AFTER truncate\n");
	fill_file(PATTERN);
	if (ftruncate(fd, NEWSIZE))
		err(1, "S1 ftruncate");
	map = mmap(NULL, FILESZ, PROT_READ, MAP_SHARED, fd, 0);
	if (map == MAP_FAILED)
		err(1, "S1 mmap");
	printf("  (file size %d, probe offsets %d < in-buffer, %d > object)\n",
	    NEWSIZE, PROBE1, PROBE2);
	report("read @70000 (page kept for last buffer)", probe_read(map + PROBE1));
	report("read @200000 (page beyond object size)", probe_read(map + PROBE2));
	report("read @66800 (past EOF, inside EOF page)", probe_read(map + 66800));
	munmap((void *)map, FILESZ);
}

static void
subtest_s2(void)
{
	volatile unsigned char *map;
	printf("S2: pre-existing mapping across truncate\n");
	fill_file(PATTERN);
	map = mmap(NULL, FILESZ, PROT_READ, MAP_SHARED, fd, 0);
	if (map == MAP_FAILED)
		err(1, "S2 mmap");
	report("read @70000 BEFORE truncate", probe_read(map + PROBE1));
	if (ftruncate(fd, NEWSIZE))
		err(1, "S2 ftruncate");
	report("read @70000 AFTER truncate", probe_read(map + PROBE1));
	munmap((void *)map, FILESZ);
}

static void
subtest_s3(void)
{
	volatile unsigned char *map;
	char c;
	ssize_t n;
	printf("S3: WRITE beyond EOF via fresh mmap after truncate\n");
	fill_file(PATTERN);
	if (ftruncate(fd, NEWSIZE))
		err(1, "S3 ftruncate");
	map = mmap(NULL, FILESZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (map == MAP_FAILED)
		err(1, "S3 mmap");
	int w = probe_write(map + PROBE1, 'X');
	printf("  write @70000 : %s\n", w == 0 ? "SUCCEEDED (no SIGBUS!)" : "SIGBUS");
	if (w == 0) {
		fsync(fd);
		if (ftruncate(fd, FILESZ))
			err(1, "S3 extend");
		n = pread(fd, &c, 1, PROBE1);
		printf("  after extend, pread @70000: n=%zd val=0x%02x (%s)\n",
		    n, (unsigned char)c,
		    c == 'X' ? "stale mapping write became file data" :
		    c == 0x00 ? "zero (extend zero-fill covered it)" : "other");
	}
	munmap((void *)map, FILESZ);
}

/* ---- S4 race ---- */
static volatile int stop;
static unsigned long hits_window, hits_zero, hits_bus, iters, fstat_small;

static off_t
cur_size(void)
{
	struct stat st;
	if (fstat(fd, &st) == 0)
		return st.st_size;
	return -1;
}

static void *
reader(void *arg __unused)
{
	volatile unsigned char *map;
	int v1, v2;
	off_t s1, s2;

	map = mmap(NULL, FILESZ, PROT_READ, MAP_SHARED, fd, 0);
	if (map == MAP_FAILED)
		err(1, "S4 reader mmap");
	while (!stop) {
		iters++;
		v1 = probe_read(map + PROBE1);
		if (v1 == PATTERN) {
			s1 = cur_size();
			if (s1 >= 0 && s1 < PROBE1) {
				fstat_small++;
				v2 = probe_read(map + PROBE1);
				s2 = cur_size();
				/*
				 * pattern served twice while the file is
				 * verified (by fstat) smaller than PROBE1.
				 * Refill writes only happen while size is
				 * large, so this can only be the truncate
				 * window (size updated by
				 * nvnode_pager_setsize before bzero runs).
				 */
				if (v2 == PATTERN && s2 < PROBE1)
					hits_window++;
				else if (v2 < 0)
					hits_bus++;
				else if (v2 == 0)
					hits_zero++;
			}
		}
	}
	munmap((void *)map, FILESZ);
	return NULL;
}

static void
subtest_s4(int seconds)
{
	pthread_t th;
	int cycles = 0;
	time_t t0;

	printf("S4: strict race window (%d s): pattern@70000 observed with "
	    "fstat-verified size < 70000\n", seconds);
	fill_file(PATTERN);
	if (pthread_create(&th, NULL, reader, NULL))
		err(1, "pthread_create");
	t0 = time(NULL);
	while (time(NULL) - t0 < (time_t)seconds) {
		/* size small: truncate-down mid-block */
		if (ftruncate(fd, NEWSIZE))
			err(1, "S4 ftruncate down");
		/* size big again, then restore pattern while in-file */
		if (ftruncate(fd, FILESZ))
			err(1, "S4 ftruncate up");
		{
			static unsigned char b[65536];
			memset(b, PATTERN, sizeof(b));
			ssize_t n = pwrite(fd, b, 65536, 65536);
			(void)n;
		}
		cycles++;
	}
	stop = 1;
	pthread_join(th, NULL);
	printf("  cycles=%d reader_iters=%lu fstat_small=%lu "
	    "confirm-zero=%lu confirm-SIGBUS=%lu "
	    "WINDOW-HITS(pattern twice while small)=%lu\n",
	    cycles, iters, fstat_small, hits_zero, hits_bus, hits_window);
}

int
main(int argc, char **argv)
{
	if (argc > 1)
		snprintf(path, sizeof(path), "%s", argv[1]);
	else
		snprintf(path, sizeof(path), "/tmp/df2920.bin");

	setvbuf(stdout, NULL, _IONBF, 0);
	install_handlers();

	fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd < 0)
		err(1, "open %s", path);

	printf("=== DF-2920: beyond-EOF fault behavior on %s ===\n", path);
	subtest_s0();
	subtest_s1();
	subtest_s2();
	subtest_s3();
	subtest_s4(argc > 2 ? atoi(argv[2]) : 8);

	close(fd);
	unlink(path);
	return 0;
}
