DF-1946 / validate_bypass.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 | /* * DF-1946 — amdgpu_ucode_validate() missing minimum-size / bounds checks. * * Source under audit: sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 * * int amdgpu_ucode_validate(const struct firmware *fw) * { * const struct common_firmware_header *hdr = * (const struct common_firmware_header *)fw->data; * if (fw->datasize == le32_to_cpu(hdr->size_bytes)) * return 0; * return -EINVAL; * } * * The function ONLY verifies that the buffer length matches the header's * self-declared size_bytes. It does NOT check: * (a) datasize >= sizeof(struct common_firmware_header) == 32, so a 4-byte * firmware whose first 4 bytes equal datasize passes; every field past * offset 4 (header_size_bytes at offset 4, ..., ucode_size_bytes at * offset 20, ucode_array_offset_bytes at offset 24, crc32 at offset 28) * is read out of bounds by every caller. * (b) ucode_array_offset_bytes is within [0, datasize), so a header can * claim a payload that begins past the end of the buffer. * (c) ucode_array_offset_bytes + ucode_size_bytes <= datasize (overflow- * safe), so callers (e.g. amdgpu_ucode_init_single_fw at L347-351) will * memcpy from a pointer past fw->data and/or past fw->data+datasize. * * This is the root gateway for 30+ callers (gfx_v7/8/9_0.c, * psp_v3_1/v10/v11_0.c, gmc_v7/8_0.c, amdgpu_uvd/vce.c, smu*_smumgr.c). * * THREAT MODEL: HW-gated. amdgpu_ucode_validate runs during amdgpu device * attach (and resume), which requires an AMD GPU to be present. On a system * WITH an AMD GPU, an attacker who can plant/replace a firmware file in the * kernel firmware search path (root, or writable /lib/firmware) can pass an * OOB-reading header through validate(); the resulting OOB read feeds the * DF-1947 OOB write. * * This harness replicates the validate() logic EXACTLY in userspace and shows * that crafted firmware images pass it while being objectively unsafe to * dereference. It then prints the offset arithmetic each caller would * perform, proving the OOB read. */ #include <stdio.h> #include <stdint.h> #include <string.h> #include <stdlib.h> /* Verbatim copy of the struct from sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26 */ struct common_firmware_header { uint32_t size_bytes; /* offset 0 */ uint32_t header_size_bytes; /* offset 4 */ uint16_t header_version_major; /* offset 8 */ uint16_t header_version_minor; /* offset 10 */ uint16_t ip_version_major; /* offset 12 */ uint16_t ip_version_minor; /* offset 14 */ uint32_t ucode_version; /* offset 16 */ uint32_t ucode_size_bytes; /* offset 20 */ uint32_t ucode_array_offset_bytes; /* offset 24 */ uint32_t crc32; /* offset 28 */ }; /* sizeof == 32 */ struct firmware { const uint8_t *data; size_t datasize; }; /* Verbatim replica of amdgpu_ucode_validate (amdgpu_ucode.c:251-260). */ static int amdgpu_ucode_validate(const struct firmware *fw) { const struct common_firmware_header *hdr = (const struct common_firmware_header *)fw->data; if (fw->datasize == hdr->size_bytes) /* le32 removed; host-endian demo */ return 0; return -22; /* -EINVAL */ } /* The "fixed" version we propose. */ static int amdgpu_ucode_validate_fixed(const struct firmware *fw) { const struct common_firmware_header *hdr = (const struct common_firmware_header *)fw->data; uint32_t size_bytes, ucode_size_bytes, ucode_array_offset_bytes; if (fw->datasize < sizeof(*hdr)) return -22; size_bytes = hdr->size_bytes; ucode_size_bytes = hdr->ucode_size_bytes; ucode_array_offset_bytes = hdr->ucode_array_offset_bytes; if (fw->datasize != size_bytes) return -22; if (ucode_array_offset_bytes > size_bytes || ucode_size_bytes > size_bytes - ucode_array_offset_bytes) return -22; return 0; } static void run_case(const char *name, const uint8_t *buf, size_t len) { struct firmware fw = { .data = buf, .datasize = len }; int rc_vanilla, rc_fixed; printf("=== %s (datasize=%zu) ===\n", name, len); rc_vanilla = amdgpu_ucode_validate(&fw); rc_fixed = amdgpu_ucode_validate_fixed(&fw); printf(" vanilla amdgpu_ucode_validate: %s (rc=%d)%s\n", rc_vanilla == 0 ? "PASS" : "reject", rc_vanilla, rc_vanilla == 0 ? " <- bug: caller will deref OOB" : ""); printf(" fixed amdgpu_ucode_validate: %s (rc=%d)\n", rc_fixed == 0 ? "pass" : "REJECT", rc_fixed); /* Replicate what callers do once validate() returns 0: they dereference * hdr->ucode_size_bytes (offset 20) and hdr->ucode_array_offset_bytes * (offset 24) and feed them to a memcpy. We compute, WITHOUT actually * reading OOB, how many bytes past fw->data the deref / memcpy would * touch. */ if (rc_vanilla == 0) { size_t need_for_fields = 28; /* up through ucode_array_offset_bytes */ size_t field_oob = (len < need_for_fields) ? (need_for_fields - len) : 0; printf(" CALLER deref of hdr->ucode_size_bytes (off 20):\n"); if (len < 24) { printf(" reads 4 bytes starting at data+20; datasize=%zu " "=> %zu-byte OOB READ past fw->data\n", len, 24 - len); } else { printf(" in-bounds by luck\n"); } if (field_oob) printf(" header field read alone needs %zu bytes past end-of-buffer\n", field_oob); if (len >= 28) { uint32_t ucode_size = ((const uint32_t *)(buf + 20))[0]; uint32_t arr_off = ((const uint32_t *)(buf + 24))[0]; uint64_t end = (uint64_t)arr_off + (uint64_t)ucode_size; printf(" CALLER memcpy(data+%u, %u bytes): end-of-copy=%llu, " "datasize=%zu => %s\n", arr_off, ucode_size, (unsigned long long)end, len, end > len ? "OOB READ (crosses end of firmware)" : "in-bounds"); } } printf("\n"); } int main(void) { uint8_t buf[256]; printf("DF-1946 harness: amdgpu_ucode_validate missing size/bounds checks\n"); printf("Reference: sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260\n"); printf("sizeof(struct common_firmware_header) = %zu\n\n", sizeof(struct common_firmware_header)); /* --- Case 1: 4-byte firmware, size_bytes==datasize. Passes validate(). */ memset(buf, 0, sizeof(buf)); ((uint32_t *)buf)[0] = 4; /* size_bytes = 4 == datasize */ run_case("CASE 1: 4-byte fw, size_bytes=datasize=4 (no header at all)", buf, 4); /* --- Case 2: 16-byte firmware, datasize==size_bytes==16. Passes; callers * dereference fields at offsets 20 and 24, both past datasize. */ memset(buf, 0, sizeof(buf)); ((uint32_t *)buf)[0] = 16; /* size_bytes = 16 */ run_case("CASE 2: 16-byte fw, size_bytes=datasize=16 " "(ucode_size_bytes field at off 20 is OOB)", buf, 16); /* --- Case 3: well-formed header size (32) but payload claims to begin * at offset 0x100 and span 0x100 bytes (i.e. far past the buffer). * datasize==size_bytes==32 -> validate PASSES; callers dereference * ucode_array_offset_bytes (0x100) and memcpy 0x100 bytes from * data+0x100, all OOB. */ memset(buf, 0, sizeof(buf)); ((uint32_t *)buf)[0] = 32; /* size_bytes = 32 */ ((uint32_t *)buf)[1] = 32; /* header_size = 32 */ ((uint32_t *)buf)[5] = 0x100;/* ucode_size_bytes = 256 */ ((uint32_t *)buf)[6] = 0x100;/* ucode_array_offset_bytes= 256 */ run_case("CASE 3: 32-byte fw, claims 256-byte payload at offset 256 " "(passes validate, OOB read in every caller)", buf, 32); /* --- Case 4: legitimately-formed 64-byte firmware with a real 32-byte * payload at offset 32. validate passes; fixed validate passes too. * Sanity baseline. */ memset(buf, 0, sizeof(buf)); ((uint32_t *)buf)[0] = 64; /* size_bytes = 64 */ ((uint32_t *)buf)[1] = 32; /* header_size = 32 */ ((uint32_t *)buf)[5] = 32; /* ucode_size_bytes = 32 */ ((uint32_t *)buf)[6] = 32; /* ucode_array_offset_bytes= 32 */ run_case("CASE 4: well-formed 64-byte fw (baseline; both pass)", buf, 64); /* --- Case 5: integer-overflow attack: ucode_array_offset_bytes = 0xFFFFFF00 * and ucode_size_bytes = 0x100, so a naive `arr_off + ucode_size` * in 32-bit wraps to 0x0 and the size check passes against datasize=32. * Vanilla validate does not check this; fixed version uses 64-bit math * so it rejects. */ memset(buf, 0, sizeof(buf)); ((uint32_t *)buf)[0] = 32; /* size_bytes = 32 == datasize */ ((uint32_t *)buf)[5] = 0x100; /* ucode_size_bytes */ ((uint32_t *)buf)[6] = 0xFFFFFF00; /* ucode_array_offset_bytes */ run_case("CASE 5: 32-byte fw with wrap-around offset+size " "(vanilla accepts, fixed rejects)", buf, 32); return 0; } |