// SPDX-License-Identifier: GPL-2.0+
/*
 * holetest -- test simultaneous page faults on hole-backed pages
 * Copyright (C) 2015  Hewlett Packard Enterprise Development LP
 */


/*
 * holetest
 *
 * gcc -Wall -pthread -o holetest holetest.c
 *
 * This test tool exercises page faults on hole-y portions of an mmapped
 * file.  The file is created, sized using various methods, mmapped, and
 * then two threads race to write a marker to different offsets within
 * each mapped page.  Once the threads have finished marking each page,
 * the pages are checked for the presence of the markers.
 *
 * The file is sized four different ways: explicitly zero-filled by the
 * test, posix_fallocate(), fallocate(), and ftruncate().  The explicit
 * zero-fill does not really test simultaneous page faults on hole-backed
 * pages, but rather serves as control of sorts.
 *
 * Usage:
 *
 *   holetest [-f] FILENAME FILESIZEinMB
 *
 * Where:
 *
 *   FILENAME is the name of a non-existent test file to create
 *
 *   FILESIZEinMB is the desired size of the test file in MiB
 *
 * If the test is successful, FILENAME will be unlinked.  By default,
 * if the test detects an error in the page markers, then the test exits
 * immediately and FILENAME is left.  If -f is given, then the test
 * continues after a marker error and FILENAME is unlinked, but will
 * still exit with a non-0 status.
 */

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <getopt.h>
#include <sys/wait.h>

#define THREADS 2

long page_size;
long page_offs[THREADS];
int use_wr[THREADS];
int prefault = 0;
int use_fork = 0;
int use_private = 0;

uint64_t get_id(void)
{
	if (!use_fork)
		return (uint64_t) pthread_self();
	return getpid();
}

void prefault_mapping(char *addr, long npages)
{
	long i;

	for (i = 0; i < npages; i++) {
		if (addr[i * page_size] != 0) {
			fprintf(stderr, "Prefaulting found non-zero value in "
				"page %ld: %d\n", i, (int)addr[i * page_size]);
		}
	}
}

int verify_mapping(char *vastart, long npages, uint64_t *expect)
{
	int errcnt = 0;
	int i;
	char *va;

	for (va = vastart; npages > 0; va += page_size, npages--) {
		for (i = 0; i < THREADS; i++) {
			if (*(uint64_t*)(va + page_offs[i]) != expect[i]) {
				printf("ERROR: thread %d, "
				       "offset %08llx, %08llx != %08llx\n", i,
				       (unsigned long long) (va + page_offs[i] - vastart),
				       (unsigned long long) *(uint64_t*)(va + page_offs[i]),
				       (unsigned long long) expect[i]);
				errcnt++;
			}
		}
	}
	return errcnt;
}

void *pt_page_marker(void *args)
{
	void **a = args;
	char *va = (char *)a[1];
	long npages = (long)a[2];
	long i;
	long pgoff = page_offs[(long)a[3]];
	uint64_t tid = get_id();
	long errors = 0;

	if (prefault && use_fork)
		prefault_mapping(va, npages);

	/* mark pages */
	for (i = 0; i < npages; i++)
		*(uint64_t *)(va + pgoff + i * page_size) = tid;

	if (use_private && use_fork) {
		uint64_t expect[THREADS] = {};

		expect[(long)a[3]] = tid;
		errors = verify_mapping(va, npages, expect);
	}

	return (void *)errors;
}  /* pt_page_marker() */

void *pt_write_marker(void *args)
{
	void **a = args;
	int fd = (long)a[0];
	long npages = (long)a[2];
	long pgoff = page_offs[(long)a[3]];
	uint64_t tid = get_id();
	long i;

	/* mark pages */
	for (i = 0; i < npages; i++)
		pwrite(fd, &tid, sizeof(tid), i * page_size + pgoff);

	return NULL;
}

int test_this(int fd, loff_t sz)
{
	long npages;
	char *vastart;
	void *targs[THREADS][4];
	pthread_t t[THREADS];
	uint64_t tid[THREADS];
	int errcnt = 0;
	int i;

	npages = sz / page_size;
	printf("INFO: sz = %llu\n", (unsigned long long)sz);

	/* mmap it */
	vastart = mmap(NULL, sz, PROT_READ | PROT_WRITE,
		       use_private ? MAP_PRIVATE : MAP_SHARED, fd, 0);
	if (MAP_FAILED == vastart) {
		perror("mmap()");
		exit(20);
	}

	if (prefault && !use_fork)
		prefault_mapping(vastart, npages);

	/* prepare the thread args */
	for (i = 0; i < THREADS; i++) {
		targs[i][0] = (void *)(long)fd;
		targs[i][1] = vastart;
		targs[i][2] = (void *)npages;
		targs[i][3] = (void *)(long)i;
	}

	for (i = 0; i < THREADS; i++) {
		if (!use_fork) {
			/* start two threads */
			if (pthread_create(&t[i], NULL,
				   use_wr[i] ? pt_write_marker : pt_page_marker,
				   &targs[i])) {
				perror("pthread_create");
				exit(21);
			}
			tid[i] = (uint64_t)t[i];
			printf("INFO: thread %d created\n", i);
		} else {
			pid_t pid;
			/*
			 * Flush stdout before fork, otherwise some lines get
			 * duplicated... ?!?!?
			 */
			fflush(stdout);
			pid = fork();
			if (pid < 0) {
				int j;

				perror("fork");
				for (j = 0; j < i; j++)
					waitpid(tid[j], NULL, 0);
				exit(21);
			} else if (!pid) {
				/* Child? */
				void *ret;

				if (use_wr[i])
					ret = pt_write_marker(&targs[i]);
				else
					ret = pt_page_marker(&targs[i]);
				exit(ret ? 1 : 0);
			}
			tid[i] = pid;
			printf("INFO: process %d created\n", i);
		}
	}

	/* wait for them to finish */
	for (i = 0; i < THREADS; i++) {
		if (!use_fork) {
			void *status;

			pthread_join(t[i], &status);
			if (status)
				errcnt++;
		} else {
			int status;

			waitpid(tid[i], &status, 0);
			if (!WIFEXITED(status) || WEXITSTATUS(status) > 0)
				errcnt++;
		}
	}

	/* check markers on each page */
	/* For private mappings & fork we should see no writes happen */
	if (use_private && use_fork)
		for (i = 0; i < THREADS; i++)
			tid[i] = 0;
	errcnt = verify_mapping(vastart, npages, tid);
	munmap(vastart, sz);

	if (use_private) {
		/* Check that no writes propagated into original file */
		for (i = 0; i < THREADS; i++)
			tid[i] = 0;
		vastart = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd, 0);
		if (vastart == MAP_FAILED) {
			perror("mmap()");
			exit(20);
		}
		errcnt += verify_mapping(vastart, npages, tid);
		munmap(vastart, sz);
	}

	printf("INFO: %d error(s) detected\n", errcnt);


	return errcnt;
}

int main(int argc, char **argv)
{
	int stoponerror = 1;
	char *path;
	loff_t sz;
	int fd;
	int errcnt;
	int toterr = 0;
	int i, step;
	char *endch;
	int opt;

	page_size = getpagesize();
	step = page_size / THREADS;
	page_offs[0] = step / 2;
	for (i = 1; i < THREADS; i++)
		page_offs[i] = page_offs[i-1] + step;

	while ((opt = getopt(argc, argv, "fwrFp")) > 0) {
		switch (opt) {
		case 'f':
			/* ignore errors */
			stoponerror = 0;
			break;
		case 'w':
			/* use writes instead of mmap for one thread */
			use_wr[0] = 1;
			break;
		case 'r':
			/* prefault mmapped area by reading it */
			prefault = 1;
			break;
		case 'F':
			/* create processes instead of threads */
			use_fork = 1;
			break;
		case 'p':
			/* Use private mappings for testing */
			use_private = 1;
			break;
		default:
			fprintf(stderr, "ERROR: Unknown option character.\n");
			exit(1);
		}
	}

	if (optind != argc - 2) {
		fprintf(stderr, "ERROR: usage: holetest [-fwrFp] "
			"FILENAME FILESIZEinMB\n");
		exit(1);
	}
	if (use_private && use_wr[0]) {
		fprintf(stderr, "ERROR: Combinations of writes and private"
			"mappings not supported.\n");
		exit(1);
	}

	path = argv[optind];
	sz = strtol(argv[optind + 1], &endch, 10);
	if (*endch || sz < 1) {
		fprintf(stderr, "ERROR: bad FILESIZEinMB\n");
		exit(1);
	}
	sz <<= 20;

	/*
	 * we're going to run our test in several different ways:
	 *
	 * 1. explictly zero-filled
	 * 2. posix_fallocated
	 * 3. ftruncated
	 */


	/*
	 * explicitly zero-filled
	 */
	printf("\nINFO: zero-filled test...\n");

	/* create the file */
	fd = open(path, O_RDWR | O_EXCL | O_CREAT, 0644);
	if (fd < 0) {
		perror(path);
		exit(2);
	}

	/* truncate it to size */
	if (ftruncate(fd, sz)) {
		perror("ftruncate()");
		exit(3);
	}

	/* explicitly zero-fill */
	{
		char*   va = mmap(NULL, sz, PROT_READ | PROT_WRITE,
				  MAP_SHARED, fd, 0);
		if (MAP_FAILED == va) {
			perror("mmap()");
			exit(4);
		}
		memset(va, 0, sz);
		munmap(va, sz);
	}

	/* test it */
	errcnt = test_this(fd, sz);
	toterr += errcnt;
	close(fd);
	if (stoponerror && errcnt > 0)
		exit(5);

	/* cleanup */
	if (unlink(path)) {
		perror("unlink()");
		exit(6);
	}


	/*
	 * posix_fallocated
	 */
	printf("\nINFO: posix_fallocate test...\n");

	/* create the file */
	fd = open(path, O_RDWR | O_EXCL | O_CREAT, 0644);
	if (fd < 0) {
		perror(path);
		exit(7);
	}

	/* fill it to size */
	if (posix_fallocate(fd, 0, sz)) {
		perror("posix_fallocate()");
		exit(8);
	}

	/* test it */
	errcnt = test_this(fd, sz);
	toterr += errcnt;
	close(fd);
	if (stoponerror && errcnt > 0)
		exit(9);

	/* cleanup */
	if (unlink(path)) {
		perror("unlink()");
		exit(10);
	}

	/*
	 * ftruncated
	 */
	printf("\nINFO: ftruncate test...\n");

	/* create the file */
	fd = open(path, O_RDWR | O_EXCL | O_CREAT, 0644);
	if (fd < 0) {
		perror(path);
		exit(15);
	}

	/* truncate it to size */
	if (ftruncate(fd, sz)) {
		perror("ftruncate()");
		exit(16);
	}

	/* test it */
	errcnt = test_this(fd, sz);
	toterr += errcnt;
	close(fd);
	if (stoponerror && errcnt > 0)
		exit(17);

	/* cleanup */
	if (unlink(path)) {
		perror("unlink()");
		exit(18);
	}

	/* done */
	if (toterr > 0)
		exit(19);
	return 0;
}
