Report the failures that used to be crashes

Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11.

Both string accessors in json_helpers.c ended their ATTEMPT block with
FINISH(errctx, false), which swallows the failure, and then strncpy'd through
the pointer akgl_heap_next_string never set. So the one condition the pool
exists to report -- it is full, which in practice means something is not
releasing -- arrived as a segfault somewhere else entirely. It is
FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and
asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the
old code, which is also how the tilemap leak test in the previous commit
confirmed this one.

akgl_tilemap_release tested layers[i].texture and destroyed
tilesets[i].texture, so every tileset texture was freed twice on one release
and no image layer's texture was freed at all. Pointers are cleared as they go,
so a second release is safe instead of a use-after-free.

akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on
frame one because fps is 0 for the first second. Only akgl_game_init installs
it, and renderer.h documents the other path deliberately -- a host that owns
its window binds a backend instead. It installs the default when it finds NULL.

akgl_controller_pushmap and akgl_controller_default checked only the upper
bound, so a negative id indexed before akgl_controlmaps.

The two test-harness helpers were quietly worthless. akgl_render_and_compare
drew t1 on both passes, so it always reported a match and every image assertion
built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd
s1->pitch * s1->h bytes out of both surfaces without checking that the second
was the same size, so a smaller one was read past its end. Both fixed, both
tested. tests/util.c also now calls the collide-point test it has defined and
never run.

25/25 pass, memcheck clean, reindent --check and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:32:11 -04:00
parent 230278d303
commit 21d69192e3
12 changed files with 309 additions and 15 deletions

View File

@@ -440,7 +440,7 @@ akerr_ErrorContext *akgl_controller_pushmap(int controlmapid, akgl_Control *cont
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(errctx, control, AKERR_NULLPOINTER, "NULL Control");
FAIL_NONZERO_BREAK(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
newmapid = akgl_controlmaps[controlmapid].nextMap;
FAIL_ZERO_BREAK(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
@@ -459,7 +459,7 @@ akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, i
PREPARE_ERROR(errctx);
ATTEMPT {
// set up the control map
FAIL_NONZERO_BREAK(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
memset((void *)&control, 0x00, sizeof(akgl_Control));
controlmap = &akgl_controlmaps[controlmapid];
controlmap->kbid = kbid;

View File

@@ -254,6 +254,14 @@ void akgl_game_update_fps(void)
akgl_game.lastFPSTime = curTime;
}
if ( akgl_game.fps < 30 ) {
// Install the default if nobody has. Only akgl_game_init sets this, and
// renderer.h documents the other path on purpose -- a host that owns
// its own window calls akgl_render_2d_bind instead. Such an embedder
// used to crash on frame one, because fps is 0 for the first second and
// 0 is under the threshold.
if ( akgl_game.lowfpsfunc == NULL ) {
akgl_game.lowfpsfunc = &akgl_game_lowfps;
}
akgl_game.lowfpsfunc();
}
akgl_game.framesSinceUpdate += 1;

View File

@@ -94,9 +94,13 @@ akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_Stri
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);
@@ -152,9 +156,13 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, false);
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx);

View File

@@ -828,14 +828,20 @@ akerr_ErrorContext *akgl_tilemap_release(akgl_Tilemap *dest)
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL map");
int i = 0;
// Each pointer is cleared as it goes. Without that a second release
// destroys textures SDL has already freed, and the second loop used to
// destroy `tilesets[i]` while testing `layers[i]` -- so every tileset
// texture was freed twice and no image layer's texture was freed at all.
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
if ( dest->tilesets[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
dest->tilesets[i].texture = NULL;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( dest->layers[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
SDL_DestroyTexture(dest->layers[i].texture);
dest->layers[i].texture = NULL;
}
}
SUCCEED_RETURN(errctx);

View File

@@ -224,6 +224,30 @@ akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
// Geometry first. The memcmp reads s1->pitch * s1->h bytes out of *both*,
// so a smaller s2 was read past its end rather than reported as a
// mismatch -- and a differing pitch or format made the comparison
// meaningless even when it did not run off.
FAIL_NONZERO_RETURN(
errctx,
((s1->w != s2->w) || (s1->h != s2->h)),
AKERR_VALUE,
"Comparison surfaces differ in size: %dx%d against %dx%d",
s1->w, s1->h, s2->w, s2->h);
FAIL_NONZERO_RETURN(
errctx,
(s1->pitch != s2->pitch),
AKERR_VALUE,
"Comparison surfaces differ in pitch: %d against %d",
s1->pitch, s2->pitch);
FAIL_NONZERO_RETURN(
errctx,
(s1->format != s2->format),
AKERR_VALUE,
"Comparison surfaces differ in pixel format: %s against %s",
SDL_GetPixelFormatName(s1->format), SDL_GetPixelFormatName(s2->format));
FAIL_ZERO_RETURN(errctx, s1->pixels, AKERR_NULLPOINTER, "First surface has no pixels");
FAIL_ZERO_RETURN(errctx, s2->pixels, AKERR_NULLPOINTER, "Second surface has no pixels");
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
SUCCEED_RETURN(errctx);
}
@@ -261,7 +285,7 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
SDL_RenderClear(akgl_renderer->sdl_renderer);
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t2, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");