Skip to content

Commit 5b73ff7

Browse files
committed
tasks: fix OOM bugs across save/load/patch/database/audio/decompress/http/wifi/steam/blissbox completion paths
Swept through tasks/ looking for unchecked malloc/calloc/realloc returns followed by dereferences that would NULL-segv on OOM. Fixes span 12 files and 22 individual sites; split by file: * task_save.c (5 fixes): - task_save_handler_finished: task_data calloc was unchecked before a memcpy(task_data, state, ...) that NULL-derefs on OOM. Now NULL-checked; on failure the task_set_data attachment is skipped and save_state_cb / undo_save_state_cb consume NULL via their own NULL checks (added below). - save_state_cb (consumer for task_save_handler_finished): previously assumed task_data non-NULL and called strdup(state->path) unconditionally. Now early-returns on NULL task_data. - content_load_state_cb (consumer for task_load_handler_ finished): previously dereferenced load_data->size / ->data at variable initialisation time. Now NULL-checks first, hoisting the field reads past the guard. - content_load_and_save_state_cb: previously dereferenced load_data->path / ->undo_data / ->undo_size / ->flags. Now delegates to content_load_state_cb on NULL (which handles NULL safely) and skips the save push. - task_load_handler_finished: on calloc failure the old code early-returned without freeing the on-thread 'state' struct or setting a task error - a pre-existing leak plus silent failure. Now frees state->data + state and sets a task error so the user sees the failure. * task_translation.c (1 fix): - PNG decode path: raw_image_data malloc unchecked; the indexed write in the conversion loop NULL-derefs. NULL-check + 'goto finish' (which NULL-safely frees raw_image_data_alpha and rpng). * task_patch.c (2 fixes): - apply_patch_xdelta: *targetdata malloc unchecked; xd3_decode_memory writes into it. NULL-check + set PATCH_TARGET_ALLOC_FAILED + goto cleanup_stream (matching the explicit ENOSPC handling below). - indexed patch scan: four mallocs for name_ips_indexed / name_bps_indexed / name_ups_indexed / name_xdelta_indexed were unchecked before four strlcpy calls that NULL-deref. Joint NULL-check returns true early (same observable result as no extra patches found). * task_database.c (1 fix): - database_info_list_iterate_found_match: db_crc + entry_path_str mallocs unchecked before the '[0] = \\0;' stores that NULL-deref. Joint NULL-check frees whichever succeeded and returns SCAN_VERDICT_ERROR so the scanner moves on to the next content entry cleanly. * task_audio_mixer.c (2 fixes): - task_push_audio_mixer_load: nbio->path strdup was unchecked; downstream strdup(nbio->path) in the ogg upload handler is UB on NULL (POSIX). NULL-check + goto error. - task_push_audio_mixer_load_and_play: identical twin pattern, identical fix. (Note: these are NOT the audio_mixer UAF that was separately reverted upstream as 4be8b20. Those were in audio/audio_driver.c; these are in the task enqueue path.) * task_decompress.c (3 fixes): - file_archived: callback_error malloc unchecked before strlcpy. NULL-gate the error-string population (skipping leaves task->error NULL, which task_set_error and downstream consumers handle). - file_decompressed: same twin pattern, same fix. - task_decompress_handler_finished: data calloc unchecked before data->source_file = ... write. On OOM free source_file (which would otherwise leak via the skipped task_set_data) and set a task error. * task_database_cue.c (3 fixes): - PS2 detect_ps2_game: disc_data malloc unchecked before intfstream_read, which writes via fread (no NULL guard on destination). NULL-check + return 0, matching the PS1/PSP twin patterns above/below which already had this check. - track_extract first variant: data malloc unchecked before intfstream_read. NULL-check + goto error. - track_extract second variant: twin pattern, twin fix. * task_http.c (1 fix): - task_http_transfer_handler: http_transfer_data_t malloc unchecked before data->data / ->len / ->headers / ->status writes. On OOM free the already-fetched 'tmp' buffer (data was about to take ownership) and set a task error. Moved 'bool mute;' declaration into the success branch to avoid an unused-variable warning on the failure branch. * task_wifi.c (1 fix): - task_push_wifi_connect: task->user_data malloc unchecked before memcpy. On OOM free task->title and the task itself, return false so the caller sees a clean failure. * task_content_disc.c (1 fix): - dump-cue DUMP_STATE_WRITE_CUE: cue_data calloc unchecked before filestream_read. NULL-gate the read; cue_data is actually unused after the read (only passed to free at the bottom of this block), so skipping the read has no further consequence beyond the failed read itself. * task_autodetect_blissbox.c (2 fixes): - lp_device_path LocalAlloc unchecked before strlcpy NULL-derefs. NULL-check + 'continue' to advance the inner SetupDiEnumDeviceInterfaces 'for (i = 0; ...; i++)' loop to the next device interface. - device_path malloc unchecked before the indexed copy loop NULL-derefs. NULL-check + free the LocalAlloc'd lp_device_path + continue. Both 'continue' statements target the inner for-loop's i++ advance clause, which is the intended behaviour (skip this interface, try the next). The outer 'while (!ret)' loop increments 'index' at the bottom of its body, which is unaffected by the inner continue. * task_steam.c (1 fix): - task_push_steam_core_dlc_install: both task_init and steam_core_dlc_install_state_t calloc were unchecked before the state->app_id / ->name / ->has_downloaded and task->handler / ->state / ->title / ->progress / ->callback writes that NULL-deref. Joint NULL-check frees whichever succeeded and returns; caller is menu Steam-DLC action with no task-return channel, so return-silently matches the pre-patch failure mode (task would have crashed or silently not installed) without the crash. (This is separate from the two other steam/steam.c bugs that are blocked on an external mist.h enum value.) All fixes follow the same pattern: NULL-check immediately after the alloc, clean up partial state on failure, and propagate the error via either task_set_error, a bool/enum return, or the next-task-iteration advance (continue). Free-on-NULL paths exploit free(NULL) being a no-op to minimise conditional branches. Thread-safety: these are all on-thread modifications within the task worker or task-dispatch thread context - task_queue.c serialises completion callbacks on the main thread before the task_data memory is read, so the NULL-toleration added in completion callbacks is safe against the earlier-dispatched worker writing NULL into task_data. Scope: all fixes are local to individual task handlers/callbacks; no API changes, no header changes, no cross-subsystem impact. The task_queue.c callback dispatch contract is unchanged - callbacks receive task_data which may be NULL if the handler was unable to allocate it, and callbacks are now consistently NULL-tolerant in the cases touched here. Reachability: every fix is reachable under OOM. The more likely user-facing cases are save_state_cb / content_load_state_cb / content_load_and_save_state_cb (triggered on every save-state / load-state action), and the blissbox enumeration (triggered on every USB joypad hotplug if blissbox support is built in).
1 parent a04f966 commit 5b73ff7

12 files changed

Lines changed: 278 additions & 48 deletions

tasks/task_audio_mixer.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,12 @@ bool task_push_audio_mixer_load_and_play(
465465
goto error;
466466

467467
nbio->path = strdup(fullpath);
468+
/* NULL-check strdup: downstream strdup(nbio->path) calls at
469+
* lines 72 and 429 assume this is non-NULL. strdup(NULL) is
470+
* UB per POSIX; glibc crashes. Fail the task setup so the
471+
* caller can surface the error. */
472+
if (!nbio->path)
473+
goto error;
468474

469475
if (!(mixer = (struct audio_mixer_handle*)calloc(1, sizeof(*mixer))))
470476
goto error;
@@ -591,6 +597,11 @@ bool task_push_audio_mixer_load(
591597
goto error;
592598

593599
nbio->path = strdup(fullpath);
600+
/* NULL-check strdup: see the twin check in the sibling
601+
* task_push_audio_mixer_load for the reasoning. strdup(NULL)
602+
* is UB and downstream code dereferences nbio->path. */
603+
if (!nbio->path)
604+
goto error;
594605

595606
if (!(mixer = (struct audio_mixer_handle*)calloc(1, sizeof(*mixer))))
596607
goto error;

tasks/task_autodetect_blissbox.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,31 @@ static const blissbox_pad_type_t* input_autoconfigure_get_blissbox_pad_type_win3
299299
size_t nLength = _tcslen(pInterfaceDetailData->DevicePath) + 1;
300300
lp_device_path = (TCHAR*)LocalAlloc(LPTR, nLength * sizeof(TCHAR));
301301

302+
/* NULL-check lp_device_path: strlcpy below NULL-derefs
303+
* on LocalAlloc failure. Skip this device; the inner
304+
* 'for (i = 0; SetupDiEnumDeviceInterfaces(...); i++)'
305+
* loop advances to the next interface index. The
306+
* outer 'while (!ret)' loop is unaffected (it keys off
307+
* SetupDiEnumDeviceInfo returns, not this iterator). */
308+
if (!lp_device_path)
309+
continue;
310+
302311
strlcpy(lp_device_path,
303312
pInterfaceDetailData->DevicePath, nLength);
304313

305314
device_path = (char*)malloc(nLength);
306315

316+
/* NULL-check device_path: the indexed write below
317+
* NULL-derefs on OOM. Free the LocalAlloc'd
318+
* lp_device_path we just populated and skip this
319+
* device. */
320+
if (!device_path)
321+
{
322+
LocalFree(lp_device_path);
323+
lp_device_path = NULL;
324+
continue;
325+
}
326+
307327
for (len = 0; len < nLength; len++)
308328
device_path[len] = lp_device_path[len];
309329

tasks/task_content_disc.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,15 @@ static void task_cdrom_dump_handler(retro_task_t *task)
148148
int64_t cue_size = filestream_get_size(state->file);
149149
char *cue_data = (char*)calloc(1, cue_size);
150150

151-
filestream_read(state->file, cue_data, cue_size);
151+
/* NULL-check: filestream_read below writes into
152+
* cue_data via fread which NULL-derefs on the
153+
* destination pointer. If alloc fails skip the
154+
* read - cue_data is actually unused after the read
155+
* (only passed to free at the bottom of this block,
156+
* line ~226), so the only consequence of the skip
157+
* is that the read happens to not be performed. */
158+
if (cue_data)
159+
filestream_read(state->file, cue_data, cue_size);
152160

153161
state->stream = filestream_get_vfs_handle(state->file);
154162
state->toc = retro_vfs_file_get_cdrom_toc();

tasks/task_database.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,20 @@ static enum scan_verdict database_info_list_iterate_found_match(
913913
database_info_t *db_info_entry =
914914
&db_state->info->list[db_state->entry_index];
915915

916+
/* NULL-check both mallocs: the 'db_crc[0] = ...' /
917+
* 'entry_path_str[0] = ...' writes below NULL-deref on OOM.
918+
* free(NULL) in the teardown at the end of the function is
919+
* safe, so we can bail early; clean up whichever succeeded
920+
* and return SCAN_VERDICT_ERROR so the scanner continues
921+
* with the next content entry rather than silently
922+
* mismatching. */
923+
if (!db_crc || !entry_path_str)
924+
{
925+
free(db_crc);
926+
free(entry_path_str);
927+
return SCAN_VERDICT_ERROR;
928+
}
929+
916930
db_crc[0] = '\0';
917931
entry_path_str[0] = '\0';
918932

tasks/task_database_cue.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ int detect_ps2_game(intfstream_t *fd, char *s, size_t len,
292292
return 0;
293293

294294
disc_data = (char*)malloc(DISC_DATA_SIZE_PS2);
295+
/* NULL-check: intfstream_read writes into disc_data via
296+
* filestream_read (which calls fread with no NULL-guard on the
297+
* dest). Matches the NULL-checked pattern in the PS1 / PSP
298+
* variants above/below. */
299+
if (!disc_data)
300+
return 0;
295301

296302
if (intfstream_read(fd, disc_data, DISC_DATA_SIZE_PS2) <= 0)
297303
{
@@ -1515,6 +1521,10 @@ bool intfstream_file_get_serial(const char *name,
15151521
goto error;
15161522

15171523
data = (uint8_t*)malloc(size);
1524+
/* NULL-check: intfstream_read below would NULL-deref on
1525+
* OOM via filestream_read's fread. */
1526+
if (!data)
1527+
goto error;
15181528

15191529
if (intfstream_read(fd, data, size) != (int64_t) size)
15201530
{
@@ -1669,6 +1679,10 @@ bool intfstream_file_get_crc_and_size(const char *name,
16691679
goto error;
16701680

16711681
data = (uint8_t*)malloc(len);
1682+
/* NULL-check: intfstream_read below would NULL-deref on
1683+
* OOM via filestream_read's fread. */
1684+
if (!data)
1685+
goto error;
16721686

16731687
if (intfstream_read(fd, data, len) != (int64_t)len)
16741688
goto error;

tasks/task_decompress.c

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,26 @@ static int file_decompressed_subdir(const char *name,
136136
return 1;
137137

138138
userdata->dec->callback_error = (char*)malloc(CALLBACK_ERROR_SIZE);
139-
_len = strlcpy(userdata->dec->callback_error,
140-
"Failed to deflate ",
141-
CALLBACK_ERROR_SIZE);
142-
_len += strlcpy(
143-
userdata->dec->callback_error + _len,
144-
path,
145-
CALLBACK_ERROR_SIZE - _len);
146-
userdata->dec->callback_error[ _len] = '.';
147-
userdata->dec->callback_error[++_len] = '\n';
148-
userdata->dec->callback_error[++_len] = '\0';
139+
/* NULL-check: the strlcpy below NULL-derefs on OOM. The
140+
* downstream task_set_error calls (lines 246, 274, 304) accept
141+
* NULL, so skipping the error-string population leaves the task
142+
* with a NULL error and the user sees no 'Failed to deflate'
143+
* message - which is strictly better than segfaulting. Any
144+
* subsequent 'if (dec->callback_error)' downstream is already
145+
* NULL-safe. */
146+
if (userdata->dec->callback_error)
147+
{
148+
_len = strlcpy(userdata->dec->callback_error,
149+
"Failed to deflate ",
150+
CALLBACK_ERROR_SIZE);
151+
_len += strlcpy(
152+
userdata->dec->callback_error + _len,
153+
path,
154+
CALLBACK_ERROR_SIZE - _len);
155+
userdata->dec->callback_error[ _len] = '.';
156+
userdata->dec->callback_error[++_len] = '\n';
157+
userdata->dec->callback_error[++_len] = '\0';
158+
}
149159

150160
return 0;
151161
}
@@ -178,13 +188,17 @@ static int file_decompressed(const char *name, const char *valid_exts,
178188
}
179189

180190
dec->callback_error = (char*)malloc(CALLBACK_ERROR_SIZE);
181-
_len = strlcpy(dec->callback_error, "Failed to deflate ",
182-
CALLBACK_ERROR_SIZE);
183-
_len += strlcpy(dec->callback_error + _len,
184-
path, CALLBACK_ERROR_SIZE - _len);
185-
dec->callback_error[ _len] = '.';
186-
dec->callback_error[++_len] = '\n';
187-
dec->callback_error[++_len] = '\0';
191+
/* NULL-check: see twin in file_archived for reasoning. */
192+
if (dec->callback_error)
193+
{
194+
_len = strlcpy(dec->callback_error, "Failed to deflate ",
195+
CALLBACK_ERROR_SIZE);
196+
_len += strlcpy(dec->callback_error + _len,
197+
path, CALLBACK_ERROR_SIZE - _len);
198+
dec->callback_error[ _len] = '.';
199+
dec->callback_error[++_len] = '\n';
200+
dec->callback_error[++_len] = '\0';
201+
}
188202

189203
return 0;
190204
}
@@ -206,8 +220,22 @@ static void task_decompress_handler_finished(retro_task_t *task,
206220
decompress_task_data_t *data =
207221
(decompress_task_data_t*)calloc(1, sizeof(*data));
208222

209-
data->source_file = dec->source_file;
210-
task_set_data(task, data);
223+
/* NULL-check: the ->source_file write below NULL-derefs on
224+
* OOM. task_set_data accepts NULL, but the consumer
225+
* (completion callback) would then see a NULL task_data.
226+
* If data alloc fails we instead set a task error and free
227+
* source_file ourselves so downstream consumers see a
228+
* clean error state rather than partial success. */
229+
if (data)
230+
{
231+
data->source_file = dec->source_file;
232+
task_set_data(task, data);
233+
}
234+
else
235+
{
236+
free(dec->source_file);
237+
task_set_error(task, strdup("Out of memory"));
238+
}
211239
}
212240

213241
if (dec->subdir)

tasks/task_http.c

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,20 +183,35 @@ static void task_http_transfer_handler(retro_task_t *task)
183183
}
184184
else
185185
{
186-
bool mute;
187186
data = (http_transfer_data_t*)malloc(sizeof(*data));
188-
data->data = tmp;
189-
data->len = _len;
190-
data->headers = net_http_headers_ex(http->handle, http->headers_accept_err);
191-
data->status = net_http_status(http->handle);
192-
193-
task_set_data(task, data);
194-
195-
mute = ((task->flags & RETRO_TASK_FLG_MUTE) > 0);
196-
197-
if (!mute && net_http_error(http->handle))
198-
task_set_error(task, strldup("Download failed.",
199-
sizeof("Download failed.")));
187+
/* NULL-check: the field writes below NULL-deref on OOM.
188+
* Free the already-fetched 'tmp' buffer (which data was
189+
* about to take ownership of), set a task error so the
190+
* caller sees a clean failure, and skip the task_set_data
191+
* attachment. */
192+
if (!data)
193+
{
194+
if (tmp)
195+
free(tmp);
196+
task_set_error(task, strldup("Out of memory.",
197+
sizeof("Out of memory.")));
198+
}
199+
else
200+
{
201+
bool mute;
202+
data->data = tmp;
203+
data->len = _len;
204+
data->headers = net_http_headers_ex(http->handle, http->headers_accept_err);
205+
data->status = net_http_status(http->handle);
206+
207+
task_set_data(task, data);
208+
209+
mute = ((task->flags & RETRO_TASK_FLG_MUTE) > 0);
210+
211+
if (!mute && net_http_error(http->handle))
212+
task_set_error(task, strldup("Download failed.",
213+
sizeof("Download failed.")));
214+
}
200215
}
201216
net_http_delete(http->handle);
202217
}

tasks/task_patch.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,16 @@ static enum patch_error xdelta_apply_patch(
689689
} while (stream.avail_in);
690690

691691
*targetdata = (uint8_t*)malloc(*targetlength);
692+
/* NULL-check: xd3_decode_memory writes into *targetdata.
693+
* Passing NULL is either a crash or an opaque xdelta error
694+
* depending on the library version. On OOM set
695+
* PATCH_TARGET_ALLOC_FAILED to match the explicit ENOSPC
696+
* error path below and skip the decode. */
697+
if (!*targetdata)
698+
{
699+
error_patch = PATCH_TARGET_ALLOC_FAILED;
700+
goto cleanup_stream;
701+
}
692702
switch (xd3_decode_memory(
693703
patchdata, patchlen,
694704
sourcedata, sourcelength,
@@ -918,6 +928,23 @@ bool patch_content(
918928
* for subsequent patches starts at 1 */
919929
size_t patch_index = 1;
920930

931+
/* NULL-check all four mallocs together: the strlcpy calls
932+
* below dereference each buffer, and the loop that follows
933+
* does indexed writes into all four. On OOM free whichever
934+
* succeeded (free(NULL) is a no-op) and skip the indexed-
935+
* patch scan entirely - returning true matches the behaviour
936+
* when no extra indexed patches are found on disk, which is
937+
* the expected outcome for nearly all content. */
938+
if ( !name_ips_indexed || !name_bps_indexed
939+
|| !name_ups_indexed || !name_xdelta_indexed)
940+
{
941+
free(name_ips_indexed);
942+
free(name_bps_indexed);
943+
free(name_ups_indexed);
944+
free(name_xdelta_indexed);
945+
return true;
946+
}
947+
921948
strlcpy(name_ips_indexed, name_ips, (name_ips_len + 1) * sizeof(char));
922949
strlcpy(name_bps_indexed, name_bps, (name_bps_len + 1) * sizeof(char));
923950
strlcpy(name_ups_indexed, name_ups, (name_ups_len + 1) * sizeof(char));

0 commit comments

Comments
 (0)