⬢ DragonFlyBSD Kernel Audit
DF-5228 / poc5228.c
← back to finding ↓ download raw
/*
 * DF-5228 PoC (v2): dma_free_coherent() permanently leaks every physical page
 * of the freed coherent allocation.
 *
 * sys/dev/drm/linux_dma.c:76-82 implements dma_free_coherent as a bare
 *	kmem_free(kernel_map, (vm_offset_t)cpu_addr, size);
 * The correct counterpart of kmem_alloc_contig()/contigmalloc is contigfree()
 * (pmap_qremove + kmem_free + vm_page_free_contig -- vm_contig.c:588-603).
 *
 * Consequences demonstrated here:
 *  (a) the kernel PTE is ORPHANED: pmap_kextract(freed_va) still returns the
 *      physical address after dma_free_coherent returned the KVA to
 *      kernel_map (vm_map_delete never pmap_removes the object-less
 *      kmem_alloc_pageable entry -- vm_map.c:3325-3370);
 *  (b) the underlying vm_page stays wired and is NEVER freed: allocations
 *      drain the 128MB low-memory DMA reserve alist first (vm_page.c
 *      vm_page_alloc_contig), then real free pages;
 *  (c) therefore an alloc+free LOOP, which with correct free semantics is
 *      sustainable forever, burns memory monotonically until the machine
 *      is out of RAM/reserve;
 *  (d) control: contigmalloc/contigfree loop recovers every page.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <vm/vm_extern.h>
#include <vm/vm_page.h>
#include <machine/pmap.h>

struct device;
extern void *dma_alloc_coherent(struct device *dev, size_t size,
    unsigned long *dma_handle, unsigned int flag);
extern void dma_free_coherent(struct device *dev, size_t size,
    void *cpu_addr, unsigned long dma_handle);

/* 4GB DMA cap, same value the shim hardcodes (BUS_SPACE_MAXADDR_32BIT) */
#define	POC_MAXADDR_32BIT	0xFFFFFFFFUL

#define	CHUNK	(2 * 1024 * 1024)
#define	CYCLES	60			/* 240MB total vs 128MB reserve */

static int
poc5228_handler(struct module *m, int what, void *arg)
{
	unsigned char *p, *probe_va = NULL;
	unsigned long dh;
	vm_paddr_t pa;
	vm_page_t mp;
	long free0, wire0, freeN;
	int i, ok_cycles = 0, orphan_seen = 0;

	if (what != MOD_LOAD)
		return 0;

	free0 = vmstats.v_free_count;
	wire0 = vmstats.v_wire_count;
	kprintf("DF5228: baseline v_free_count=%ld v_wire_count=%ld\n",
	    free0, wire0);

	/* (a)+(b): single cycle, probe the freed VA and its vm_page */
	p = dma_alloc_coherent(NULL, CHUNK, &dh, 0 /* GFP_KERNEL */);
	if (p) {
		probe_va = p;
		dma_free_coherent(NULL, CHUNK, p, dh);
		pa = pmap_kextract((vm_offset_t)probe_va);
		orphan_seen = (pa != 0);
		kprintf("DF5228: after free: pmap_kextract(va)=%016jx -> "
		    "kernel PTE %s\n", (intmax_t)pa,
		    pa ? "STILL VALID (ORPHANED)" : "gone");
		if (pa) {
			mp = PHYS_TO_VM_PAGE(pa);
			kprintf("DF5228: phys page state: wire_count=%d "
			    "flags=%08x (PG_FICTITIOUS=%d) queue=%d -> "
			    "page %s\n", mp->wire_count, mp->flags,
			    !!(mp->flags & PG_FICTITIOUS), mp->queue,
			    (mp->wire_count ? "STILL WIRED (LEAKED)" :
			    "released"));
		}
	}

	/* (c): sustainable-loop test. With a correct free this runs forever
	 * with zero net memory change. With the leak it drains the reserve
	 * (128MB, invisible to vmstats) and then real free pages. */
	for (i = 0; i < CYCLES; i++) {
		p = dma_alloc_coherent(NULL, CHUNK, &dh, 0);
		if (p == NULL) {
			kprintf("DF5228: alloc failed at cycle %d "
			    "(allocation no longer sustainable)\n", i);
			break;
		}
		dma_free_coherent(NULL, CHUNK, p, dh);
		ok_cycles++;
		if ((i % 30) == 29)
			kprintf("DF5228: %d cycles: v_free_count=%ld "
			    "(net deficit %ld pages = %ldMB)\n",
			    i + 1, (long)vmstats.v_free_count,
			    free0 - (long)vmstats.v_free_count,
			    (free0 - (long)vmstats.v_free_count) >> 8);
	}
	freeN = vmstats.v_free_count;
	kprintf("DF5228 RESULT: %d MB alloc+freed (%d cycles of %dKB): "
	    "v_free_count deficit=%ld pages (%ldMB), v_wire surplus=%ld\n",
	    (ok_cycles * CHUNK) / (1024*1024), ok_cycles, CHUNK / 1024,
	    free0 - freeN, (free0 - freeN) >> 8,
	    (long)vmstats.v_wire_count - wire0);

	/* (d): control loop -- correct API, must be loss-free */
	{
		unsigned char *c;
		long fb;

		fb = vmstats.v_free_count;
		for (i = 0; i < 20; i++) {
			c = contigmalloc(CHUNK, M_DEVBUF, M_WAITOK, 0,
			    POC_MAXADDR_32BIT, PAGE_SIZE, 0);
			if (c == NULL)
				break;
			contigfree(c, CHUNK, M_DEVBUF);
		}
		kprintf("DF5228: control %d x contigmalloc/contigfree: "
		    "free deficit=%ld pages (expected 0)\n",
		    i, fb - (long)vmstats.v_free_count);
	}

	if (orphan_seen && free0 - freeN > (CYCLES * CHUNK / PAGE_SIZE) / 4) {
		kprintf("DF5228 VERDICT: REPRODUCED - dma_free_coherent "
		    "orphaned the kernel PTE and never released the wired "
		    "page; %dMB of alloc+free cycles (zero live "
		    "allocations) permanently burned %ldMB of free memory "
		    "plus ~128MB of DMA-reserve capacity\n",
		    (ok_cycles * CHUNK) / (1024*1024), (free0 - freeN) >> 8);
	} else {
		kprintf("DF5228 VERDICT: not reproduced "
		    "(orphan_seen=%d deficit=%ld)\n",
		    orphan_seen, free0 - freeN);
	}
	return (0);
}

static moduledata_t poc5228_mod = {
	"poc5228",
	poc5228_handler,
	NULL
};
DECLARE_MODULE(poc5228, poc5228_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);