Release the error context when a path resolves against its root

akgl_path_relative took its ENOENT branch by returning from inside the
HANDLE block, which skips the RELEASE_ERROR that FINISH ends with. The
handled context was never given back, so one entry of AKERR_ARRAY_ERROR
was lost per call and the 129th call hit "Unable to pull an error context
from the array!" and exited the process.

That branch is not an edge case: every asset path inside a tilemap is
resolved relative to the map's own directory, so it is taken several times
per map load. A game that loaded fifty levels died in the loader. It was
found by a benchmark that loaded the fixture map in a loop, which is the
first thing in this tree to call it more than a hundred times.

The fallback now runs after FINISH, so the context is released on the way
past. tests/util.c resolves AKERR_MAX_ARRAY_ERROR * 2 paths through the
branch and asserts the pool is where it started -- against the old code
that test does not fail, it terminates the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:23:47 -04:00
parent f35443e84d
commit c6227545a6
2 changed files with 84 additions and 3 deletions

View File

@@ -99,6 +99,7 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
PREPARE_ERROR(e);
akgl_String *strbuf;
char *result;
bool relative_to_root = false;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
@@ -115,10 +116,20 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
IGNORE(akgl_heap_release_string(strbuf));
} PROCESS(e) {
} HANDLE(e, ENOENT) {
// Path is not relative to our current working directory
// Noop - execution proceeds after the break
return akgl_path_relative_root(root, path, dst);
// Path is not relative to our current working directory. Resolve it
// against root instead -- but after FINISH, not from in here. Returning
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
// with, so the handled context is never given back to
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
// call takes the whole process down with "Unable to pull an error
// context from the array!". Every map load resolves several paths this
// way.
relative_to_root = true;
} FINISH(e, true);
if ( relative_to_root == true ) {
PASS(e, akgl_path_relative_root(root, path, dst));
}
SUCCEED_RETURN(e);
}