Bound every array a data file can index

Closes Defects items 16 and 17 and Known-and-still-open item 6. All three let
an asset file, or a caller's argument, write past a fixed array.

akgl_sprite_load_json took its frame count straight from the document and wrote
that many entries into a 16-byte frameids -- through a uint32_t * cast of a
uint8_t *, so each write touched four bytes and the overrun reached four bytes
past the array, into the rest of akgl_Sprite and then the next pool slot. The
count is checked first now, each id is read into an int and narrowed
deliberately, and a frame number too large for a uint8_t is refused rather than
truncated into an index for a different tile.

The tilemap loader had the same shape twice: objects[j] with no check against
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER and tilesets[i] with none against
AKGL_TILEMAP_MAX_TILESETS. akgl_tilemap_load_layers already bounded its own
loop, so the pattern was in the same file. The object one is the reachable
half -- 128 objects is not a large object layer.

akgl_string_initialize zeroed sizeof(akgl_String) starting at `data`, which
begins after the refcount in front of it, so it ran four bytes past the end of
the object and onto the *next* slot's refcount -- the field the allocator reads
to decide whether a slot is free. Same file, same class, fixed with it:
akgl_string_copy accepted a count larger than the buffers, reading past one
pool slot and writing past another, which the header documented as behaviour.

Every case has a test that fails against the old code, with five new fixtures.
Exactly-the-maximum is asserted alongside one-past in each, so the bound cannot
be fixed by making the limit off by one.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:24:35 -04:00
parent 6b1cf437d3
commit 230278d303
14 changed files with 3763 additions and 39 deletions

View File

@@ -14,7 +14,12 @@ akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
} else {
memset(&obj->data, 0x00, sizeof(akgl_String));
// sizeof(obj->data), not sizeof(akgl_String). `data` starts after the
// `refcount` in front of it, so zeroing the size of the whole struct
// from the start of the buffer ran four bytes past the end of the
// object -- into the next pool slot's refcount, which is what makes a
// free slot look claimed or a claimed one look free.
memset(&obj->data, 0x00, sizeof(obj->data));
}
obj->refcount = 1;
SUCCEED_RETURN(errctx);
@@ -28,6 +33,16 @@ akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dest, int co
if ( count == 0 ) {
count = AKGL_MAX_STRING_LENGTH;
}
// Both buffers are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count
// walks off the end of two pool slots at once. Refused rather than
// documented, which is what it used to be.
FAIL_NONZERO_RETURN(
errctx,
((count < 0) || (count > AKGL_MAX_STRING_LENGTH)),
AKERR_OUTOFBOUNDS,
"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");
}