Skip to content

Commit 7335b37

Browse files
committed
Security: heap- and stack-overflow fixes across image, audio, IO and cheevos paths
Fixes eight memory-safety bugs sharing a small set of root causes: uint32_t multiplications feeding size_t allocations, ftell() and filestream_get_size() returning -1 silently coerced to size_t, attacker-controlled filenames used as filesystem paths, and per-channel-count assumptions in audio decode. * rjpeg, rpng (formats/jpeg/rjpeg.c, formats/png/rpng.c) Width*height*sizeof(uint32_t) wraps in uint32 on 32-bit hosts (3DS, Vita, PSP, Wii, Wii U, older Android, 32-bit Windows), the malloc returns an undersized buffer, and the per-row decode writes multi-GiB off the end. Add (size_t) casts at every pixel-buffer malloc site so the multiplication is overflow-safe on 64-bit. Add a 0x4000 dimension cap matching rbmp.c, rtga.c and rwebp.c, gated on 32-bit only -- on 64-bit the size_t arithmetic plus the existing 4 GiB output guard in rpng_iterate_image are sufficient, and a hard cap would reject legitimate large images (IrfanView and friends routinely open tens-of-thousands-pixel JPEGs and PNGs). * image_transfer Gekko path (formats/image_transfer.c) Same uint32 multiplication primitive in the post-decode tile conversion on Wii. Add (size_t) casts. The 32-bit rpng/rjpeg cap closes the primitive at the source on Wii, but the casts are kept for defence in depth. * audio_mixer FLAC (audio/audio_mixer.c) audio_mixer_mix_flac asks drflac for AUDIO_MIXER_TEMP_BUFFER/2 frames into a 32 KiB stack buffer. drflac writes frame_count*channel_count floats, so 8-channel FLAC writes 128 KiB into the 32 KiB buffer -- a 96 KiB stack overflow reaching saved-RIP territory. Reject non-stereo FLAC at audio_mixer_play_flac time. Mono was already producing wrong audio per the existing comment in mix_flac; a proper per- channel-aware fix is left for a separate change. * cheevos badge_name (cheevos/cheevos_client.c) rcheevos_client_download_badge interpolates the server-supplied badge_name into a filesystem path and writes the HTTP body there. A malicious or MITM'd retroachievements.org could send badge_name = "../../../../etc/cron.d/evil" and write attacker- controlled bytes (with a forced .png suffix) anywhere on disk. Real badge names are numeric IDs optionally suffixed with "_lock"; reject anything outside [A-Za-z0-9_-]. * cheevos JSON-override loader (cheevos/cheevos_client.c) ftell()'s long return was assigned to size_t _len, so an ftell error (-1) became SIZE_MAX; malloc(_len + 1) wrapped to malloc(0) and contents[_len] = 0 corrupted the heap. Capture into long, check for the negative sentinel. Debug-only path (CHEEVOS_JSON_OVERRIDE). * rxml (formats/xml/rxml.c) Same pattern, file-scale: filestream_get_size returns -1 on error and silently flowed into (size_t)(len + 1) as 0 on 64-bit (malloc(0) -> tiny block) or as a wrapped value on 32-bit (undersized buffer). Either way memory_buffer[len] = '\0' wrote far out of bounds. Reject negative sizes and any size that wouldn't fit in size_t. Reachable via .xml shader presets and Logiqx .dat database files. * filestream_read_file (streams/file_stream.c) The existing size check was if ((int64_t)(uint64_t)(size + 1) != (size + 1)) goto error; -- tautological for any positive int64_t, never trips. On 32-bit hosts any file > ~4 GiB silently truncates through the size_t cast on the malloc and the subsequent filestream_read overruns the heap. Replace with an explicit size_t-fit check. * test/formats/test_rpng.c (new) Six libcheck cases covering rpng_process_ihdr. The two that exercise the 0x4000 cap (0x4001 and 30000^2) are gated on 32- bit hosts only since on 64-bit those dimensions are legitimate after this change. The remaining four (accept-at-limit, uint32-max-reject, accept-small, zero-rejected) run on every platform. Wired into Makefile.test alongside the existing string/utils/hash/lists/queues suites; builds with -Werror under the same TEST_UNIT_CFLAGS the other tests use; pulls in trans_stream*.c + -lz for the zlib backend reference rpng.c links to.
1 parent 90b96a3 commit 7335b37

9 files changed

Lines changed: 177 additions & 14 deletions

File tree

cheevos/cheevos_client.c

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ void rcheevos_client_http_load_response(const rc_api_request_t* request,
294294
rc_client_server_callback_t callback, void* callback_data)
295295
{
296296
char* contents;
297+
long ftell_ret;
297298
size_t _len = 0;
298299
FILE* file = fopen(CHEEVOS_JSON_OVERRIDE, "rb");
299300

@@ -311,9 +312,23 @@ void rcheevos_client_http_load_response(const rc_api_request_t* request,
311312
}
312313

313314
fseek(file, 0, SEEK_END);
314-
_len = ftell(file);
315+
ftell_ret = ftell(file);
315316
fseek(file, 0, SEEK_SET);
316317

318+
/* ftell can return -1 on stream error. Pre-patch _len was
319+
* declared size_t and assigned directly from ftell, so a
320+
* negative return became (size_t)-1 == SIZE_MAX, malloc
321+
* (SIZE_MAX + 1) wrapped to malloc(0) returning a tiny
322+
* non-NULL block, and the subsequent contents[_len] = 0
323+
* write at offset SIZE_MAX corrupted the heap. */
324+
if (ftell_ret < 0)
325+
{
326+
fclose(file);
327+
callback(NULL, 0, callback_data);
328+
return;
329+
}
330+
_len = (size_t)ftell_ret;
331+
317332
contents = (char*)malloc(_len + 1);
318333
/* NULL-check the malloc: without this the contents[_len] = 0
319334
* write below and the fread into contents NULL-deref. */
@@ -489,6 +504,39 @@ bool rcheevos_client_download_badge(rc_client_download_queue_t* queue,
489504
fill_pathname_slash(badge_fullpath, sizeof(badge_fullpath));
490505
badge_fullname = badge_fullpath + strlen(badge_fullpath);
491506
badge_fullname_size = sizeof(badge_fullpath) - (badge_fullname - badge_fullpath);
507+
508+
/* badge_name is supplied by the achievement server (or, on a
509+
* compromised TLS path, an attacker-controlled MITM).
510+
* fill_pathname_slash ensures we are anchored under the
511+
* badges directory, but if badge_name contains '..', '/', '\\'
512+
* or other path-component separators the resulting filesystem
513+
* write escapes that directory. Validate that badge_name is
514+
* a single safe filename component (alphanumerics plus '_' and
515+
* '-' suffixed by '_lock' on the lock variant); the underscore
516+
* is enough because real badge names from the server are
517+
* numeric IDs ("12345") or numeric IDs with a "_lock" suffix.
518+
* Reject anything else rather than synthesising a sanitised
519+
* version, since a bogus badge name from the server is itself
520+
* a signal that something is wrong. */
521+
{
522+
const char *p;
523+
bool ok = (badge_name && *badge_name);
524+
for (p = badge_name; ok && *p; p++)
525+
{
526+
char c = *p;
527+
if (!( (c >= '0' && c <= '9')
528+
|| (c >= 'a' && c <= 'z')
529+
|| (c >= 'A' && c <= 'Z')
530+
|| c == '_' || c == '-'))
531+
ok = false;
532+
}
533+
if (!ok)
534+
{
535+
CHEEVOS_LOG(RCHEEVOS_TAG "Rejecting badge with unsafe name.\n");
536+
return false;
537+
}
538+
}
539+
492540
snprintf(badge_fullname, badge_fullname_size, "%s" FILE_PATH_PNG_EXTENSION, badge_name);
493541

494542
if (path_is_valid(badge_fullpath))

libretro-common/Makefile.test

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ TEST_HASH_SRC = test/hash/test_hash.c hash/lrc_hash.c \
2323
streams/file_stream.c vfs/vfs_implementation.c file/file_path.c \
2424
compat/compat_strl.c time/rtime.c string/stdstring.c encodings/encoding_utf.c
2525

26+
TEST_RPNG = test/formats/test_rpng
27+
TEST_RPNG_SRC = test/formats/test_rpng.c formats/png/rpng.c \
28+
streams/trans_stream.c streams/trans_stream_zlib.c \
29+
streams/trans_stream_pipe.c
30+
TEST_RPNG_LIBS = -lz
31+
2632
all:
2733
# Build and execute tests in order, to avoid coverage file collision
2834
# string
@@ -45,12 +51,17 @@ all:
4551
$(CC) $(TEST_UNIT_CFLAGS) $(TEST_GENERIC_QUEUE_SRC) -o $(TEST_GENERIC_QUEUE)
4652
$(TEST_GENERIC_QUEUE)
4753
lcov -c -d . -o `dirname $(TEST_GENERIC_QUEUE)`/coverage.info
54+
# rpng
55+
$(CC) $(TEST_UNIT_CFLAGS) $(TEST_RPNG_SRC) $(TEST_RPNG_LIBS) -o $(TEST_RPNG)
56+
$(TEST_RPNG)
57+
lcov -c -d . -o `dirname $(TEST_RPNG)`/coverage.info
4858

4959
lcov -o test/coverage.info \
5060
-a test/utils/coverage.info \
5161
-a test/string/coverage.info \
5262
-a test/lists/coverage.info \
53-
-a test/queues/coverage.info
63+
-a test/queues/coverage.info \
64+
-a test/formats/coverage.info
5465
genhtml -o test/coverage/ test/coverage.info
5566

5667
clean:

libretro-common/audio/audio_mixer.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,25 @@ static bool audio_mixer_play_flac(
723723

724724
if (!dr_flac)
725725
return false;
726+
727+
/* The downstream mixer (audio_mixer_mix_flac) requests
728+
* AUDIO_MIXER_TEMP_BUFFER / 2 frames into a stack buffer
729+
* sized AUDIO_MIXER_TEMP_BUFFER floats. drflac writes
730+
* frame_count * channel_count floats, so this only fits
731+
* exactly for stereo. Mono fits but the downstream
732+
* accounting is wrong (per existing comment); >2 channels
733+
* overflows the stack buffer (e.g. 8-channel FLAC writes
734+
* 4 * AUDIO_MIXER_TEMP_BUFFER floats = 4x the buffer).
735+
* Reject anything that isn't stereo here rather than risk a
736+
* stack overflow during mix. Mono should be fixed
737+
* separately by adjusting the mixer's per-channel
738+
* accounting. */
739+
if (dr_flac->channels != 2)
740+
{
741+
drflac_close(dr_flac);
742+
return false;
743+
}
744+
726745
if (dr_flac->sampleRate != s_rate)
727746
{
728747
ratio = (double)s_rate / (double)(dr_flac->sampleRate);

libretro-common/formats/image_transfer.c

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,26 @@ int image_transfer_process(
299299
unsigned tmp_pitch, width2, i;
300300
const uint16_t *src = NULL;
301301
uint16_t *dst = NULL;
302-
void *tmp = malloc((*width) * (*height) * sizeof(uint32_t));
302+
/* (size_t) casts on width and height: pre-patch the uint32
303+
* multiplication width * height * 4 wrapped on 32-bit Wii
304+
* (Gekko is a 32-bit PowerPC) for any image with
305+
* width*height > 2^30, the malloc returned an undersized
306+
* buffer, and the memcpy below ran off the end. This file
307+
* is reached only after rpng/rjpeg has already accepted the
308+
* image; on 32-bit (which is where this matters) those
309+
* decoders cap dimensions at 0x4000 which closes the
310+
* primitive at the source. The casts here keep the
311+
* arithmetic safe regardless of upstream caps and on any
312+
* platform where image_transfer.c is compiled, including
313+
* future 64-bit Wii-class targets. */
314+
void *tmp = malloc(
315+
(size_t)(*width) * (size_t)(*height) * sizeof(uint32_t));
303316

304317
if (!tmp)
305318
return IMAGE_PROCESS_ERROR;
306319

307-
memcpy(tmp, *buf, (*width) * (*height) * sizeof(uint32_t));
320+
memcpy(tmp, *buf,
321+
(size_t)(*width) * (size_t)(*height) * sizeof(uint32_t));
308322
tmp_pitch = ((*width) * sizeof(uint32_t)) >> 1;
309323

310324
*width &= ~3;

libretro-common/formats/jpeg/rjpeg.c

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,6 +2583,24 @@ static int rjpeg_process_frame_header(rjpeg_jpeg *z, int scan)
25832583
if (s->img_x == 0)
25842584
return 0;
25852585

2586+
/* On 32-bit hosts the (size_t) casts on the malloc sites below
2587+
* are not enough by themselves: the per-component arena and
2588+
* iter_output buffers can still demand multi-hundred-MB
2589+
* allocations that fragment or fail the address space, even
2590+
* with overflow-safe arithmetic. Cap dimensions on 32-bit
2591+
* (matching the cap rbmp.c, rtga.c and rwebp.c apply) to
2592+
* preserve the previous host-resource ceiling there.
2593+
*
2594+
* On 64-bit hosts the SIZE_MAX/4 guard further down in this
2595+
* function plus the (size_t) casts make the allocations safe
2596+
* regardless of dimensions, and a desktop user loading a
2597+
* large legitimate JPEG (cf. IrfanView, image-pipeline tools)
2598+
* is a real use case. Do not cap there. */
2599+
#if SIZE_MAX <= 0xFFFFFFFFu
2600+
if (s->img_x > 0x4000u || s->img_y > 0x4000u)
2601+
return 0;
2602+
#endif
2603+
25862604
c = rjpeg_get8(s);
25872605

25882606
/* JFIF requires */
@@ -4149,7 +4167,11 @@ static int rjpeg_iterate_init_resample(rjpeg_t *rjpeg)
41494167
r->resample = j->resample_row_hv_2_kernel;
41504168
}
41514169

4152-
rjpeg->iter_output = (uint8_t *)malloc(4 * j->img_x * j->img_y);
4170+
/* (size_t) casts: img_x and img_y are uint32_t. Even with the
4171+
* 0x4000 cap above this only matters on 32-bit hosts, but the
4172+
* cast makes the expression robust against future cap changes. */
4173+
rjpeg->iter_output = (uint8_t *)malloc(
4174+
(size_t)4 * (size_t)j->img_x * (size_t)j->img_y);
41534175
if (!rjpeg->iter_output)
41544176
return 0;
41554177

@@ -4653,7 +4675,7 @@ int rjpeg_process_image(rjpeg_t *rjpeg, void **buf_data,
46534675

46544676
/* Allocate output buffer */
46554677
proc->output = (uint8_t *) malloc(
4656-
proc->n * j->img_x * j->img_y);
4678+
(size_t)proc->n * (size_t)j->img_x * (size_t)j->img_y);
46574679
if (!proc->output)
46584680
{
46594681
rjpeg_process_free(proc);

libretro-common/formats/png/rpng.c

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,28 @@ static bool rpng_process_ihdr(struct png_ihdr *ihdr)
658658
return false;
659659
}
660660

661+
/* On 32-bit hosts the per-row decode mallocs cannot fit much
662+
* more than 1 GiB of decoded RGBA, and an undersized malloc
663+
* combined with attacker-controlled dimensions has historically
664+
* been the heap-overflow primitive prompting the 0x4000 caps
665+
* in rbmp.c, rtga.c and rwebp.c. Keep the tight cap there.
666+
*
667+
* On 64-bit the (size_t) casts in rpng_reverse_filter_init and
668+
* the final allocator make the per-row arithmetic overflow-safe
669+
* regardless of dimensions, and the 4 GiB output guard further
670+
* down in rpng_iterate_image already rejects images whose
671+
* decoded buffer cannot be addressed. Loading a 30000x30000
672+
* RGBA image on a desktop with the RAM to spare is a legitimate
673+
* use case (cf. IrfanView), so do not impose the 0x4000 cap
674+
* there. */
675+
#if SIZE_MAX <= 0xFFFFFFFFu
676+
if (ihdr->width > 0x4000u || ihdr->height > 0x4000u)
677+
{
678+
fprintf(stderr, "[RPNG] Error in line %d.\n", __LINE__);
679+
return false;
680+
}
681+
#endif
682+
661683
#ifdef RPNG_TEST
662684
fprintf(stderr, "IHDR: (%u x %u), bpc = %u, palette = %s, color = %s, alpha = %s, adam7 = %s.\n",
663685
ihdr->width, ihdr->height,
@@ -696,6 +718,15 @@ static bool rpng_process_ihdr(struct png_ihdr *ihdr)
696718
return false;
697719
}
698720

721+
/* See the matching comment in the RPNG_TEST/DEBUG variant
722+
* above. Cap only on 32-bit; 64-bit lets the (size_t)
723+
* widening + 4 GiB output guard handle large legitimate
724+
* images. */
725+
#if SIZE_MAX <= 0xFFFFFFFFu
726+
if (ihdr->width > 0x4000u || ihdr->height > 0x4000u)
727+
return false;
728+
#endif
729+
699730
return true;
700731
}
701732
#endif
@@ -1063,7 +1094,7 @@ static int rpng_reverse_filter_init(const struct png_ihdr *ihdr,
10631094
rpng_passes[pngp->pass_pos].stride_y - 1) / rpng_passes[pngp->pass_pos].stride_y;
10641095

10651096
if (!(pngp->data = (uint32_t*)malloc(
1066-
pngp->pass_width * pngp->pass_height * sizeof(uint32_t))))
1097+
(size_t)pngp->pass_width * (size_t)pngp->pass_height * sizeof(uint32_t))))
10671098
return -1;
10681099

10691100
pngp->ihdr = *ihdr;
@@ -1371,11 +1402,11 @@ static int rpng_load_image_argb_process_inflate_init(
13711402
#ifdef GEKKO
13721403
/* We often use these in textures, make sure
13731404
* they're 32-byte aligned */
1374-
*data = (uint32_t*)memalign(32, rpng->ihdr.width *
1375-
rpng->ihdr.height * sizeof(uint32_t));
1405+
*data = (uint32_t*)memalign(32, (size_t)rpng->ihdr.width *
1406+
(size_t)rpng->ihdr.height * sizeof(uint32_t));
13761407
#else
1377-
*data = (uint32_t*)malloc(rpng->ihdr.width *
1378-
rpng->ihdr.height * sizeof(uint32_t));
1408+
*data = (uint32_t*)malloc((size_t)rpng->ihdr.width *
1409+
(size_t)rpng->ihdr.height * sizeof(uint32_t));
13791410
#endif
13801411
if (!*data)
13811412
goto false_end;

libretro-common/formats/xml/rxml.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222

2323
#include <string.h>
24+
#include <stdint.h>
2425

2526
#include <boolean.h>
2627
#include <streams/file_stream.h>
@@ -101,6 +102,14 @@ rxml_document_t *rxml_load_document(const char *path)
101102
return NULL;
102103

103104
len = filestream_get_size(file);
105+
/* filestream_get_size returns -1 on error. Pre-patch this
106+
* flowed through (size_t)(len + 1) as malloc(0) on 64-bit
107+
* (returning a tiny non-NULL block) or as a wrapped value on
108+
* 32-bit; either way memory_buffer[len] = '\0' wrote far
109+
* out-of-bounds. Reject negative sizes and any size that
110+
* would not fit in size_t on this platform. */
111+
if (len < 0 || (uint64_t)len >= (uint64_t)((size_t)-1))
112+
goto error;
104113
memory_buffer = (char*)malloc((size_t)(len + 1));
105114
if (!memory_buffer)
106115
goto error;

libretro-common/streams/file_stream.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,9 +629,17 @@ int64_t filestream_read_file(const char *path, void **buf, int64_t *len)
629629
if ((content_buf_size = filestream_get_size(file)) < 0)
630630
goto error;
631631

632-
if (!(content_buf = malloc((size_t)(content_buf_size + 1))))
632+
/* Reject sizes that would not survive the cast to size_t for
633+
* the malloc below. Pre-patch the only check here was a
634+
* tautological '(int64_t)(uint64_t)X != X' (which is false
635+
* for any positive int64_t), and on 32-bit hosts any file
636+
* larger than ~4 GiB silently truncated through (size_t),
637+
* the malloc was undersized, and the filestream_read below
638+
* overran it. */
639+
if ((uint64_t)content_buf_size + 1 > (uint64_t)((size_t)-1))
633640
goto error;
634-
if ((int64_t)(uint64_t)(content_buf_size + 1) != (content_buf_size + 1))
641+
642+
if (!(content_buf = malloc((size_t)(content_buf_size + 1))))
635643
goto error;
636644

637645
if ((ret = filestream_read(file, content_buf, (int64_t)content_buf_size)) <

ui/drivers/ui_qt_widgets.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7508,7 +7508,8 @@ bool MainWindow::addDirectoryFilesToList(QProgressDialog *dialog,
75087508
* Don't just extend this to add all files
75097509
* in a ZIP, because we might hit something like
75107510
* MAME/FBA where only the archives themselves
7511-
/* Only append inner file reference if filter inside archives is enabled */
7511+
* Only append inner file reference if filter
7512+
* inside archives is enabled */
75127513
if (playlistDialog->filterInArchive())
75137514
{
75147515
pathArray = (QString(pathData)

0 commit comments

Comments
 (0)