Skip to content

Commit 60bda54

Browse files
committed
cheevos/cheevos_client: fix OOM NULL-deref in badge download taskdata and debug JSON override
Two unchecked malloc sites in cheevos_client.c plus two unchecked operations in the CHEEVOS_JSON_OVERRIDE debug path. === rcheevos_client_download_badge: taskdata malloc === The badge download path allocates a task-data struct and immediately writes three fields before dispatching the HTTP task: taskdata = (rc_client_download_task_data_t*)malloc(sizeof(*taskdata)); taskdata->queue = queue; strlcpy(taskdata->badge_fullpath, badge_fullpath, sizeof(taskdata->badge_fullpath)); strlcpy(taskdata->badge_name, badge_name, sizeof(taskdata->badge_name)); task_push_http_transfer_with_user_agent(url, true, "GET", rcheevos_locals->user_agent_core, rcheevos_client_download_task_callback, taskdata); The malloc was unchecked. Two failure modes on OOM: 1. The three field writes immediately NULL-deref. 2. If the writes somehow didn't crash (they do), task_push_http_transfer_with_user_agent would dispatch the download task with a NULL taskdata user-data pointer. The completion callback rcheevos_client_download_task_callback dereferences it via callback_data->queue and callback_data->badge_fullpath - so the crash would surface much later on the task thread rather than at the alloc site, making the OOM bug look like an unrelated task-queue or HTTP-layer issue. Fix: NULL-check the malloc and return false on failure. The caller treats false as 'download failed', matching the behaviour of the path_is_valid() early-return above which returns false when the badge is already cached. When invoked via the badge queue (rcheevos_client_fetch_next_ badge), the queue continues with the next badge on the next poll. === CHEEVOS_JSON_OVERRIDE debug path: fopen and malloc === rcheevos_client_http_load_response is a debug-only function compiled in when CHEEVOS_JSON_OVERRIDE is defined to a file path that supplies a canned response. The pre-patch code was FILE* file = fopen(CHEEVOS_JSON_OVERRIDE, "rb"); fseek(file, 0, SEEK_END); _len = ftell(file); ... contents = (char*)malloc(_len + 1); fread((void*)contents, 1, _len, file); ... contents[_len] = 0; callback(contents, 200, callback_data); with no NULL handling on either fopen or malloc. A missing or unreadable override file, or an OOM on the contents alloc, would segfault inside the debug path. This is developer-only scaffolding (not compiled in any shipping configuration), but the bugs are trivial and easy to trip over when flipping the macro on during development. Fix: NULL-check the fopen with an immediate callback(NULL, 0, callback_data) + return to match the zeroed-response contract used by the production callers when the HTTP layer fails, and NULL-check the malloc with the same early-return path (plus fclose() of the opened file). === Swept-clean in the same pass === Verified NULL-checked in cheevos/: - cheevos_rvz.c: 22 alloc sites, all NULL-checked. This file has been carefully audited previously. - cheevos_client.c other 2 sites: contents malloc (now guarded by this commit); rc_client_http_task data malloc (guarded by prior audit commit with its own comment in-place); download queue calloc (guarded by prior audit commit). - cheevos.c: descriptors malloc (guarded); retry info malloc (guarded with proper free-task cleanup); shotname malloc (guarded by 'if (shotname)' wrap); cdfs_file_t malloc (intentionally unchecked - cdfs_open_file tolerates NULL file and returns 0, causing the caller to fall through to CHEEVOS_FREE(NULL) + cdfs_close_track cleanup). - cheevos_menu.c: 2 sites - menuitems realloc (tmp-pattern, NULL-checked, rolls back menuitem_capacity on failure) and malloc branch (NULL-checked, resets capacity to 0 on failure). Thread-safety: the download_badge path runs on the main menu thread at badge queue poll time. The JSON_OVERRIDE path runs synchronously on whichever thread called rcheevos_client_ server_call - but since it's a debug-only path, no concurrency concerns worth tracking. Reachability: the download_badge OOM is reachable on normal game load with Hardcore Achievements enabled - the badge fetch queue fires on every newly-unlocked achievement and each call takes this path. OOM here typically surfaces when a large badge queue is being processed on a memory-starved embedded target. The JSON_OVERRIDE crash is only reachable during development.
1 parent 31103bb commit 60bda54

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

cheevos/cheevos_client.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,32 @@ void rcheevos_client_http_load_response(const rc_api_request_t* request,
297297
size_t _len = 0;
298298
FILE* file = fopen(CHEEVOS_JSON_OVERRIDE, "rb");
299299

300+
/* NULL-check the fopen: this is a debug-only override path
301+
* (only compiled when CHEEVOS_JSON_OVERRIDE is defined) but
302+
* a missing or unreadable override file would NULL-deref the
303+
* subsequent fseek/ftell/fread calls. Match the failure
304+
* contract used elsewhere in this file by invoking the
305+
* callback with a zeroed/empty response so rc_client doesn't
306+
* hang waiting for a reply. */
307+
if (!file)
308+
{
309+
callback(NULL, 0, callback_data);
310+
return;
311+
}
312+
300313
fseek(file, 0, SEEK_END);
301314
_len = ftell(file);
302315
fseek(file, 0, SEEK_SET);
303316

304317
contents = (char*)malloc(_len + 1);
318+
/* NULL-check the malloc: without this the contents[_len] = 0
319+
* write below and the fread into contents NULL-deref. */
320+
if (!contents)
321+
{
322+
fclose(file);
323+
callback(NULL, 0, callback_data);
324+
return;
325+
}
305326
fread((void*)contents, 1, _len, file);
306327
fclose(file);
307328

@@ -480,6 +501,17 @@ bool rcheevos_client_download_badge(rc_client_download_queue_t* queue,
480501
#endif
481502

482503
taskdata = (rc_client_download_task_data_t*)malloc(sizeof(*taskdata));
504+
/* NULL-check: the field writes below NULL-deref on OOM, and
505+
* task_push_http_transfer_with_user_agent would dispatch a
506+
* task callback with a NULL taskdata that
507+
* rcheevos_client_download_task_callback dereferences via
508+
* callback_data->queue / badge_fullpath. On OOM return
509+
* false - the caller treats this as a download failure and
510+
* the queue (if present) continues with the next badge when
511+
* prompted, matching the path_is_valid() early-return at
512+
* line 473 above. */
513+
if (!taskdata)
514+
return false;
483515
taskdata->queue = queue;
484516
strlcpy(taskdata->badge_fullpath, badge_fullpath, sizeof(taskdata->badge_fullpath));
485517
strlcpy(taskdata->badge_name, badge_name, sizeof(taskdata->badge_name));

0 commit comments

Comments
 (0)