Commit e68d4ab
committed
libretro-common/formats: harden rtga + rbmp against hostile image headers
Audit of the TGA (403 lines) and BMP (577 lines) image
decoders. Both are stb_image-derived and both accept
attacker-controlled bytes from files on disk, over HTTP
(thumbnails, cores, assets bundles), or from user uploads.
Nine fixes this round; one is a directly reachable stack
buffer overflow.
Nothing touches rjpeg.c (4808 lines) or rpng.c (1859 lines);
those are their own rounds.
=== rtga.c ===
rtga: reject bogus palette_bits at the header (CRITICAL)
A single attacker-controlled byte in the TGA header
(tga_palette_bits, byte 7) drove a stack buffer overflow.
The indexed-pixel read loop did:
for (j = 0; j * 8 < tga_palette_bits; ++j)
raw_data[j] = tga_palette[pal_idx + j];
where raw_data is declared as "unsigned char raw_data[4]"
on the stack. With tga_palette_bits = 255 the loop writes
raw_data[0..31] -- up to 28 bytes of stack corruption per
pixel, driven directly by the header. Reachable on any
ingest path that accepts TGA (core-provided asset bundles,
RetroAchievement badges, user thumbnails, network mirrors).
Fix: validate tga_palette_bits at the header-check point.
TGA palettes legitimately hold only 15/16/24/32-bit entries;
anything else is rejected. Also reject tga_palette_len < 1
(empty palette) which pre-patch malloc(0)'d and then read
tga_palette[0] OOB.
rtga: rtga_skip() clamp
rtga_skip advanced img_buffer by n without checking against
img_buffer_end. Pointer arithmetic past the end of the
input array is undefined behaviour in C even without a
deref. Clamp to the remaining input; subsequent
rtga_get8 calls check "buffer < buffer_end" and parse as
EOF.
rtga: size_t output allocation + ceiling
malloc((size_t)tga_width * tga_height * sizeof(uint32_t))
was OK on 64-bit but could wrap on 32-bit for the full
65535 x 65535 header-expressible range. Also, even when
the arithmetic doesn't wrap, a legitimate-looking
65535 x 65535 ARGB decode would request ~17 GiB of memory
from a single malicious 18-byte header.
Fix: reject dimensions above 0x4000 x 0x4000 (16384 pixels
each axis = 1 GiB decoded RGBA, larger than any realistic
libretro asset). Do the malloc math in size_t.
rtga: reject size > INT_MAX in rtga_process_image
rtga_process_image cast "size" (size_t) to int before
passing it to rtga_load_from_memory. A file >= 2 GiB
truncated, propagated a negative len into
img_buffer_end = buffer + len, and gave pointer arithmetic
outside the source object. Reject early.
rtga: size_t indexing in output writes
output[dst_row * tga_width + cur_col] used signed int for
the index. 65534 * 65535 overflows int32, UB. Use size_t.
=== rbmp.c ===
rbmp: reject img_y = 0x80000000 (abs(INT_MIN) UB)
Pre-patch the code did
flip_vertically = ((int) s->img_y) > 0;
s->img_y = abs((int) s->img_y);
When img_y == 0x80000000u, the cast to int is INT_MIN and
abs(INT_MIN) is explicitly UB in C99. On GCC with 2's-
complement it returns INT_MIN and the uint32_t reassignment
leaves img_y == 0x80000000 -- a "negative" height that then
drives the output allocation to a very large value.
Fix: detect this specific value before the abs() call and
bail cleanly.
rbmp: size_t output allocation + ceiling
malloc(s->img_x * s->img_y * sizeof(uint32_t)) did the
multiplication in uint32_t (both operands are uint32_t),
so e.g. 0x10001 x 0x10000 silently wrapped to 0x10000,
producing a 256 KiB buffer for 16 GiB of claimed image
data. The subsequent per-row pixel writes ran off the
end for gigabytes -- heap corruption driven by a 14-byte
BMP header.
Fix: same ceiling as rtga (0x4000 x 0x4000) and size_t
math for the malloc.
rbmp: rbmp_skip() clamp
Same as rtga_skip.
rbmp: size_t indexing in output writes
output + dst_row * s->img_x used signed int * uint32
(promoted to uint32), which wraps. Use size_t.
=== Regression tests (two new) ===
libretro-common/samples/formats/tga/rtga_test.c
1. test_malicious_palette_bits (palette_bits=255) --
header check must reject.
2. test_empty_indexed_palette -- TRUE DISCRIMINATOR
under ASan. Pre-patch the unpatched decoder fires:
AddressSanitizer: heap-buffer-overflow
READ of size 1 at rtga.c:286 in rtga_tga_load
(1-byte region allocated at rtga.c:234)
Post-patch the decoder returns IMAGE_PROCESS_ERROR at
the header check.
3. test_happy_path_rgb -- 2x2 uncompressed BGR smoke.
4. test_oversized_size -- size > INT_MAX rejection.
libretro-common/samples/formats/bmp/rbmp_test.c
1. test_img_y_int_min -- 0x80000000 height, post-patch
rejection. Pre-patch would invoke UB via abs(INT_MIN)
and the downstream allocation decisions are
unpredictable.
2. test_dimension_overflow -- 0x10001 x 0x10000, post-
patch rejection. The pre-patch 32-bit wrap is not
reproducible on a 64-bit CI host but the rejection IS
reproducible everywhere and prevents the 17 GiB
allocation attempt that would otherwise OOM the test.
3. test_happy_1x1_24bpp -- happy-path smoke.
Builds clean under -Wall -Werror -std=gnu99. ASan -O0
clean on patched; the header-check rejections fire before
any memory is touched, so ASan never sees anything.
CI: both new tests added to RUN_TARGETS. Full samples
workflow dry-run:
Built: 17 Ran: 17 Failed: 0
VC6 compatibility: both files are compiled on all
platforms. The fix uses only size_t / uint32_t / int and
the existing stdint shim; <limits.h> (C89) is explicitly
included where needed. No new dependencies.
Reachability summary:
* Stack BOF via TGA palette_bits: HIGH severity, single
header byte, any TGA ingest path.
* Heap BOF via BMP dimension wrap: MEDIUM severity on
32-bit, reaches malloc-failure on 64-bit.
* Other fixes: UB hardening + defense-in-depth.1 parent 1c04787 commit e68d4ab
7 files changed
Lines changed: 583 additions & 15 deletions
File tree
- .github/workflows
- libretro-common
- formats
- bmp
- tga
- samples/formats
- bmp
- tga
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| 70 | + | |
69 | 71 | | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
73 | 75 | | |
74 | | - | |
75 | | - | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
76 | 84 | | |
77 | 85 | | |
78 | 86 | | |
| |||
193 | 201 | | |
194 | 202 | | |
195 | 203 | | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
196 | 212 | | |
197 | | - | |
| 213 | + | |
198 | 214 | | |
199 | 215 | | |
200 | 216 | | |
| |||
298 | 314 | | |
299 | 315 | | |
300 | 316 | | |
301 | | - | |
302 | | - | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
303 | 336 | | |
304 | 337 | | |
305 | 338 | | |
| |||
342 | 375 | | |
343 | 376 | | |
344 | 377 | | |
345 | | - | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
346 | 383 | | |
347 | 384 | | |
348 | 385 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
68 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
69 | 82 | | |
70 | 83 | | |
71 | 84 | | |
| |||
121 | 134 | | |
122 | 135 | | |
123 | 136 | | |
124 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
125 | 148 | | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
126 | 156 | | |
| 157 | + | |
127 | 158 | | |
128 | 159 | | |
129 | 160 | | |
130 | 161 | | |
131 | 162 | | |
132 | 163 | | |
133 | 164 | | |
134 | | - | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
135 | 179 | | |
136 | 180 | | |
137 | 181 | | |
| |||
226 | 270 | | |
227 | 271 | | |
228 | 272 | | |
229 | | - | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
230 | 278 | | |
231 | 279 | | |
232 | 280 | | |
233 | 281 | | |
234 | | - | |
| 282 | + | |
| 283 | + | |
235 | 284 | | |
236 | 285 | | |
237 | 286 | | |
238 | 287 | | |
239 | 288 | | |
240 | | - | |
241 | | - | |
| 289 | + | |
242 | 290 | | |
243 | 291 | | |
244 | 292 | | |
| |||
325 | 373 | | |
326 | 374 | | |
327 | 375 | | |
328 | | - | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
329 | 379 | | |
330 | | - | |
| 380 | + | |
331 | 381 | | |
332 | 382 | | |
333 | 383 | | |
| |||
366 | 416 | | |
367 | 417 | | |
368 | 418 | | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
369 | 428 | | |
370 | 429 | | |
371 | 430 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
0 commit comments