DF-5228 / poc5228.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | /* * 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); |