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:
39
src/sprite.c
39
src/sprite.c
@@ -119,6 +119,8 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
akgl_String *spritename = NULL;
|
||||
akgl_String *filename_copy = NULL;
|
||||
int i = 0;
|
||||
int framecount = 0;
|
||||
int frameid = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
@@ -161,9 +163,40 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
||||
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
|
||||
obj->frames = json_array_size((json_t *)frames);
|
||||
for ( i = 0 ; i < obj->frames; i++ ) {
|
||||
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
|
||||
// Bounded before anything is written. frameids is
|
||||
// AKGL_SPRITE_MAX_FRAMES bytes, and this loop used to take its count
|
||||
// straight from the document -- so a definition with seventeen frames
|
||||
// wrote past the array, past the rest of akgl_Sprite, and into the
|
||||
// neighbouring pool slot.
|
||||
framecount = (int)json_array_size((json_t *)frames);
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(framecount > AKGL_SPRITE_MAX_FRAMES),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sprite %s declares %d frames; the maximum is %d",
|
||||
(char *)&obj->name,
|
||||
framecount,
|
||||
AKGL_SPRITE_MAX_FRAMES
|
||||
);
|
||||
obj->frames = framecount;
|
||||
for ( i = 0 ; i < framecount; i++ ) {
|
||||
// Read into an int and narrow deliberately. The old form wrote
|
||||
// through a uint32_t * cast of a uint8_t *, so every element write
|
||||
// touched four bytes; it only appeared to work because the next
|
||||
// iteration overwrote the spill and the last one landed in the
|
||||
// struct's alignment padding.
|
||||
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, &frameid));
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((frameid < 0) || (frameid > UINT8_MAX)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sprite %s frame %d is %d; frame numbers are 0..%d",
|
||||
(char *)&obj->name,
|
||||
i,
|
||||
frameid,
|
||||
UINT8_MAX
|
||||
);
|
||||
obj->frameids[i] = (uint8_t)frameid;
|
||||
}
|
||||
} CLEANUP {
|
||||
// The sprite copies every field it wants out of the document and the
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -238,6 +238,17 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
|
||||
for (i = 0; i < json_array_size((json_t *)tilesets); i++) {
|
||||
// The bound goes at the top of the body, matching
|
||||
// akgl_tilemap_load_layers. Without it a map with seventeen
|
||||
// tilesets wrote past the fixed table -- and unlike the layer
|
||||
// count, nothing anywhere else was checking this one.
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(i >= AKGL_TILEMAP_MAX_TILESETS),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Map declares more than %d tilesets",
|
||||
AKGL_TILEMAP_MAX_TILESETS
|
||||
);
|
||||
CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset));
|
||||
CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname));
|
||||
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i));
|
||||
@@ -312,6 +323,14 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
|
||||
len = json_array_size((json_t *)layerdata);
|
||||
curlayer = &dest->layers[layerid];
|
||||
for ( j = 0; j < len; j++ ) {
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Object layer %d has more than %d objects",
|
||||
layerid,
|
||||
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
|
||||
);
|
||||
PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
|
||||
curobj = &curlayer->objects[j];
|
||||
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
|
||||
|
||||
Reference in New Issue
Block a user