Build clean under -Wall, with -Werror in CI only

TODO.md item 37 said the cast sweep buys nothing until the build turns on the
warnings those casts suppress. This is that precondition, and the argument
turned out to be right with evidence: -Wall found three genuine signedness
mismatches in sprite.c, where uint32_t width, height and speed were passed
straight to akgl_get_json_integer_value(..., int *). A cast would have silenced
all three. Fixing them properly also turned up that speed is scaled by a million
into a 32-bit field, so anything past 4294 ms overflowed rather than being held.

The other real finding was ten -Wstringop-truncation warnings, every one a
fixed-width strncpy. Those leave a name field unterminated when the input fills
it -- and those fields are registry keys, handed to strcmp and SDL property
calls that do not stop at the field. Same overread class fixed twice already
this release. All ten now use aksl_strncpy from akstdlib, which always
terminates and never NUL-pads, bounded to size - 1 so an over-long name still
truncates rather than being refused. staticstring.h documented the old strncpy
semantics explicitly and has been rewritten to match.

Two sites in game.c are deliberately left on strncpy: both stage into a
pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding
is part of the format. Neither is flagged.

sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated
string, which reads past the end of any shorter name. Not flagged by anything;
found while converting its neighbours.

-Werror is an option, default OFF, on only in the cmake_build and performance CI
jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that --
so a target-level -Werror turns every diagnostic a newer compiler invents into a
broken build for somebody else. The warning set is not even stable across build
types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not,
because they need the middle end. The two jobs that set it are one of each.

-Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the
design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must
match a signature, and 4 -Wimplicit-fallthrough from libakerror's own
PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as
deps/libakerror TODO item 8; the submodule bump carries it.

deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE
options reach it. Exempted with -w so a future semver update cannot fail this
build -- verified by forcing -Wextra -Werror and watching our files fail while
semver compiled clean.

Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror;
-Werror actually fails on a planted unused variable; an embedded consumer with
AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean,
reindent --check, check_api_surface and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 09:18:52 -04:00
parent b899f09fc8
commit 4d80664cd9
16 changed files with 230 additions and 62 deletions

View File

@@ -7,6 +7,7 @@
#include <SDL3_image/SDL_image.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/physics.h>
#include <akgl/game.h>
@@ -24,7 +25,11 @@ akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "akgl_actor_initialize received null name string pointer");
memset(obj, 0x00, sizeof(akgl_Actor));
strncpy((char *)obj->name, name, AKGL_ACTOR_MAX_NAME_LENGTH);
// aksl_strncpy always terminates; strncpy at exactly the field width does
// not, and this field is a registry key. n is one less than the field so an
// over-long name still truncates rather than being refused, which is the
// documented contract.
PASS(errctx, aksl_strncpy((char *)obj->name, sizeof(obj->name), name, sizeof(obj->name) - 1));
obj->curSpriteReversing = false;
obj->scale = 1.0;
obj->movement_controls_face = true;

View File

@@ -6,6 +6,7 @@
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <akstdlib.h>
#include <string.h>
#include <jansson.h>
@@ -24,7 +25,9 @@ akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
memset(obj, 0x00, sizeof(akgl_Character));
strncpy(obj->name, name, AKGL_CHARACTER_MAX_NAME_LENGTH);
// Always terminated: this is a registry key, and strncpy at exactly the
// field width leaves an over-long name unterminated.
PASS(errctx, aksl_strncpy(obj->name, sizeof(obj->name), name, sizeof(obj->name) - 1));
obj->state_sprites = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");

View File

@@ -523,7 +523,6 @@ static akerr_ErrorContext *load_objectnamemap(FILE *fp, SDL_PropertiesID map, in
void *ptr = NULL;
char ptrstring[32];
char objname[namelength];
int retval = 0;
bool done = false;
PREPARE_ERROR(errctx);

View File

@@ -6,6 +6,7 @@
#include <jansson.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/json_helpers.h>
#include <akgl/game.h>
@@ -102,7 +103,11 @@ akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_Stri
} PROCESS(errctx) {
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
PASS(errctx, aksl_strncpy(
(char *)&(*dest)->data,
sizeof((*dest)->data),
json_string_value(value),
sizeof((*dest)->data) - 1));
SUCCEED_RETURN(errctx);
}
@@ -164,7 +169,11 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
} PROCESS(errctx) {
} FINISH(errctx, true);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
PASS(errctx, aksl_strncpy(
(char *)&(*dest)->data,
sizeof((*dest)->data),
json_string_value(value),
sizeof((*dest)->data) - 1));
SUCCEED_RETURN(errctx);
}

View File

@@ -233,7 +233,6 @@ akerr_ErrorContext *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterat
akerr_ErrorContext *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
{
uint32_t hashval;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, type, AKERR_NULLPOINTER, "type");

View File

@@ -8,6 +8,7 @@
#include <string.h>
#include <jansson.h>
#include <akerror.h>
#include <akstdlib.h>
#include <libgen.h>
#include <akgl/game.h>
@@ -121,6 +122,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
int i = 0;
int framecount = 0;
int frameid = 0;
int jsonint = 0;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
@@ -128,7 +130,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
CATCH(errctx, akgl_heap_next_string(&filename_copy));
FAIL_NONZERO_BREAK(
errctx,
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
(strlen(filename) >= AKGL_MAX_STRING_LENGTH),
AKERR_OUTOFBOUNDS,
"Sprite filename exceeds temporary string capacity"
);
@@ -155,10 +157,43 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
(akgl_SpriteSheet *)sheet)
);
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
obj->speed = obj->speed * AKGL_TIME_ONEMS_NS;
// Read into an int and narrow deliberately. These three fields are
// uint32_t and the accessor takes an int *, so passing them directly was
// a signedness mismatch -- the kind -Wpointer-sign exists to catch, and
// the kind a cast would have hidden.
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &jsonint));
FAIL_NONZERO_BREAK(
errctx,
(jsonint <= 0),
AKERR_VALUE,
"Sprite %s has width %d; a sprite must be at least one pixel wide",
(char *)&obj->name,
jsonint);
obj->width = (uint32_t)jsonint;
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &jsonint));
FAIL_NONZERO_BREAK(
errctx,
(jsonint <= 0),
AKERR_VALUE,
"Sprite %s has height %d; a sprite must be at least one pixel high",
(char *)&obj->name,
jsonint);
obj->height = (uint32_t)jsonint;
// speed is milliseconds in the file and nanoseconds in the struct, and
// the struct field is 32 bits, so anything past this overflows on the
// multiply below rather than being held.
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &jsonint));
FAIL_NONZERO_BREAK(
errctx,
((jsonint < 0) || ((uint32_t)jsonint > (UINT32_MAX / AKGL_TIME_ONEMS_NS))),
AKERR_OUTOFBOUNDS,
"Sprite %s has a frame speed of %d ms; the range is 0 to %u",
(char *)&obj->name,
jsonint,
(UINT32_MAX / AKGL_TIME_ONEMS_NS));
obj->speed = ((uint32_t)jsonint) * AKGL_TIME_ONEMS_NS;
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
@@ -226,7 +261,10 @@ akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_Sp
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
memset(spr, 0x00, sizeof(akgl_Sprite));
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
// Was a memcpy of the full field width from a NUL-terminated string, which
// reads past the end of any name shorter than the field. Copies only what
// is there now, and terminates.
PASS(errctx, aksl_strncpy(spr->name, sizeof(spr->name), name, sizeof(spr->name) - 1));
spr->sheet = sheet;
FAIL_ZERO_RETURN(
errctx,
@@ -251,7 +289,7 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
CATCH(errctx, aksl_strncpy((char *)&sheet->name, sizeof(sheet->name), filename, sizeof(sheet->name) - 1));
sheet->texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, filename);
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());

View File

@@ -4,15 +4,15 @@
*/
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/staticstring.h>
#include <errno.h>
akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
PASS(errctx, aksl_strncpy((char *)&obj->data, sizeof(obj->data), init, sizeof(obj->data) - 1));
} else {
// sizeof(obj->data), not sizeof(akgl_String). `data` starts after the
// `refcount` in front of it, so zeroing the size of the whole struct
@@ -43,8 +43,6 @@ akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dest, int co
"Copy count %d is outside 0..%d",
count,
AKGL_MAX_STRING_LENGTH);
if ( (char *)dest->data != strncpy((char *)&dest->data, (char *)&src->data, count) ) {
FAIL_RETURN(errctx, errno, "strncpy");
}
PASS(errctx, aksl_strncpy((char *)&dest->data, sizeof(dest->data), (char *)&src->data, count));
SUCCEED_RETURN(errctx);
}

View File

@@ -149,14 +149,21 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
PASS(errctx, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
PASS(errctx, akgl_heap_next_string(&tmppath));
ATTEMPT {
strncpy((char *)&dest->tilesets[tsidx].name,
(char *)&tmpstr->data,
AKGL_TILEMAP_MAX_TILESET_NAME_SIZE
);
CATCH(errctx, aksl_strncpy(
(char *)&dest->tilesets[tsidx].name,
sizeof(dest->tilesets[tsidx].name),
(char *)&tmpstr->data,
sizeof(dest->tilesets[tsidx].name) - 1
));
CATCH(errctx, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
CATCH(errctx, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
CATCH(errctx, aksl_strncpy(
(char *)&dest->tilesets[tsidx].imagefilename,
sizeof(dest->tilesets[tsidx].imagefilename),
tmppath->data,
sizeof(dest->tilesets[tsidx].imagefilename) - 1
));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_string(tmppath));
@@ -346,7 +353,12 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
// non-NULL destination without taking another reference. Any other
// claim in between would have been handed the same string.
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
CATCH(errctx, aksl_strncpy(
(char *)curobj->name,
sizeof(curobj->name),
tmpstr->data,
sizeof(curobj->name) - 1
));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));