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:
@@ -184,6 +184,58 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A sprite definition must not be able to write past `frameids`.
|
||||
*
|
||||
* `frameids` is AKGL_SPRITE_MAX_FRAMES bytes and the loader took 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 next pool slot. It also
|
||||
* wrote through a `uint32_t *` cast of a `uint8_t *`, four bytes at a time,
|
||||
* which is why the overrun reached four bytes past the array rather than one.
|
||||
*
|
||||
* The neighbouring slot is claimed and stamped first, so the test fails on the
|
||||
* corruption rather than on whatever the corruption happens to do later.
|
||||
*/
|
||||
akerr_ErrorContext *test_akgl_sprite_load_json_bounds_frames(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Sprite *loaded = NULL;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
// Exactly the maximum is legal and must still load.
|
||||
TEST_EXPECT_OK(errctx, akgl_sprite_load_json("assets/testsprite_maxframes.json"),
|
||||
"loading a sprite with exactly AKGL_SPRITE_MAX_FRAMES frames");
|
||||
loaded = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_maxframes", NULL);
|
||||
FAIL_ZERO_BREAK(errctx, loaded, AKERR_KEY, "the max-frames sprite is not in the registry");
|
||||
TEST_ASSERT(errctx, loaded->frames == AKGL_SPRITE_MAX_FRAMES,
|
||||
"max-frames sprite loaded %d frames, expected %d",
|
||||
loaded->frames, AKGL_SPRITE_MAX_FRAMES);
|
||||
for ( i = 0; i < AKGL_SPRITE_MAX_FRAMES; i++ ) {
|
||||
TEST_ASSERT(errctx, loaded->frameids[i] == (uint8_t)i,
|
||||
"max-frames sprite frame %d is %d, expected %d",
|
||||
i, loaded->frameids[i], i);
|
||||
}
|
||||
|
||||
// One more than the maximum is refused, and nothing is registered.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_sprite_load_json("assets/testsprite_toomanyframes.json"),
|
||||
"loading a sprite with more frames than the array holds");
|
||||
TEST_ASSERT(errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_toomanyframes", NULL) == NULL,
|
||||
"a sprite with too many frames was registered anyway");
|
||||
|
||||
// A frame number a uint8_t cannot hold is refused rather than truncated
|
||||
// to something that indexes a different tile.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_sprite_load_json("assets/testsprite_widecount.json"),
|
||||
"loading a sprite whose frame number does not fit a uint8_t");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -211,6 +263,7 @@ int main(void)
|
||||
CATCH(errctx, test_akgl_spritesheet_initialize());
|
||||
CATCH(errctx, test_akgl_sprite_initialize());
|
||||
CATCH(errctx, test_akgl_sprite_load_json());
|
||||
CATCH(errctx, test_akgl_sprite_load_json_bounds_frames());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
Reference in New Issue
Block a user