Skip to content

runtime: survive a failed allocation while activating a set - #559

Open
scullymi wants to merge 5 commits into
RetroAchievements:developfrom
scullymi:fix-out-of-memory
Open

scullymi wants to merge 5 commits into
RetroAchievements:developfrom
scullymi:fix-out-of-memory

Conversation

@scullymi

Copy link
Copy Markdown
Contributor

When malloc, calloc or realloc fails while a set is activated, the runtime usually crashes or leaks. The worst case is rc_preparse_reserve_memrefs(): it links a memref list whose items failed to allocate, so the memrefs of later activations are written to address 0.

With this change every failed allocation ends in RC_OUT_OF_MEMORY for that one item, and the runtime stays usable. There are three commits: the memref lists, the parse state after an error (nothing more is allocated once the offset holds an error, and the result of RC_ALLOC is checked where it is used right away), and the activate functions in rc_runtime.c (realloc, capacity, preparse state, rich presence).

Tested with a harness that fails the n-th allocation, once for every n, under ASan and LSan. The input was 40 achievements, 40 leaderboards and a rich presence script with a lookup, and separately two real sets. Before the change, 316 of 320 and 272 of 355 failure points crashed or leaked. After it there are none, and activating everything again afterwards gives the same events as a run without a failure. The test suite passes as before (x86, x64, valgrind).

rc_runtime_init() doesn't check its malloc either. That would need an API change, so it's left as is. I can add the harness as a test if you like.

Found on an RP2350 (Pico 2 W), where a set activated next to a parsed one ran out of heap.

rc_preparse_reserve_memrefs() linked a new list into the pool even when its items could not be allocated, so later memrefs were written to address 0. It also linked the new list after the first list with room left, which dropped the lists after that one.
rc_alloc_memref() and rc_alloc_modified_memref() used a scratch list that failed to allocate, and rc_preparse_copy_memrefs() went on copying after an error.
…ations

rc_alloc() and rc_alloc_scratch() went on once the offset held an error: the size was added to the error code, and in the second pass the next object was placed in front of the buffer. A failed allocation of the first scratch instance of a type was not reported, and rc_alloc_str() left a node without a value in the tree.
The parsers check the result of RC_ALLOC where they use it right away.
…emory

A failed realloc overwrote the triggers or lboards array with NULL, and the capacity grew before the allocation succeeded. The early returns of the three activate functions leaked the preparse state, and a failed rich presence buffer left a half initialized entry behind. A failed memref reservation is checked before the second parse.

@Jamiras Jamiras left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these changes look okay. I did leave a comment recommending the comments suggesting the return value state be removed.

I'll do a more thorough review once the merge conflicts are resolved (sorry about that).

Comment thread src/runtime/rc_memref.c Outdated
memref_list->items = RC_ALLOC_ARRAY_SCRATCH(rc_memref_t, 8, parse);
if (!memref_list->items) {
memref_list->count = memref_list->capacity = 0;
return NULL; /* parse->offset is RC_OUT_OF_MEMORY */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should be in the if, not the return:

   if (!memref_list->items) { /* parse->offset is RC_OUT_OF_MEMORY */
      memref_list->count = memref_list->capacity = 0;
      return NULL;
   }

But I'd lean towards not having the comment at all. It might also be RC_INSUFFICIENT_BUFFER

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment applies to several other locations in this PR.

The conflicts in rc_alloc.c and rc_richpresence.c come from RetroAchievements#557: the checks are now made in the new rc_alloc() and rc_alloc_scratch(). Since RetroAchievements#557 the scratch pointer array is allocated on first use, so the first RC_ALLOC of a preparse can fail too: the root objects are checked before they are parsed.
@scullymi

Copy link
Copy Markdown
Contributor Author

Thanks! I merged develop in and resolved the conflicts. Since #557 the scratch pointer array is allocated on first use, so the first RC_ALLOC of a preparse can fail as well, and the root objects are now checked before they are parsed. The return value comments are gone (22de37a). The fault injection run is clean again on the merged code, and the test suite passes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rc_runtime_activate_richpresence leaves a half-initialised entry when the buffer allocation fails

2 participants