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>
271 lines
8.3 KiB
C
271 lines
8.3 KiB
C
#include <string.h>
|
|
#include <akerror.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/staticstring.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
void reset_string_heap(void);
|
|
|
|
akerr_ErrorContext *test_fresh_heap_gives_strings(void)
|
|
{
|
|
akgl_String *ptr = NULL;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&ptr));
|
|
} CLEANUP {
|
|
reset_string_heap();
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
|
|
{
|
|
akgl_String *ptr;
|
|
PREPARE_ERROR(errctx);
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
akgl_heap_strings[i].refcount = 1;
|
|
}
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&ptr));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
|
return 0;
|
|
} FINISH(errctx, true);
|
|
}
|
|
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Expected AKERR_NULLPOINTER when accessing beyond string heap bounds");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_string_heap_honors_refcount(void)
|
|
{
|
|
akgl_String *firstptr = &akgl_heap_strings[0];
|
|
akgl_String *secondptr = &akgl_heap_strings[1];
|
|
akgl_String *testptr = NULL;
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&testptr));
|
|
if ( testptr != firstptr ) {
|
|
FAIL_RETURN(
|
|
errctx,
|
|
AKERR_VALUE,
|
|
"Expected testptr to equal (akgl_heap_strings[0] = %p) but got %p",
|
|
firstptr,
|
|
testptr
|
|
);
|
|
}
|
|
CATCH(errctx, akgl_string_initialize(testptr, NULL));
|
|
if ( testptr->refcount == 0 ) {
|
|
FAIL_RETURN(errctx, AKERR_VALUE, "Expected string reference count to be nonzero but got 0");
|
|
}
|
|
if ( testptr != firstptr ) {
|
|
FAIL_RETURN(
|
|
errctx,
|
|
AKERR_VALUE,
|
|
"Expected testptr to equal (akgl_heap_strings[1] = %p) but got %p",
|
|
secondptr,
|
|
testptr
|
|
);
|
|
}
|
|
CATCH(errctx, akgl_heap_next_string(&testptr));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
|
|
{
|
|
char copybuf[AKGL_MAX_STRING_LENGTH];
|
|
akgl_String *ptr;
|
|
memset((void *)©buf, 'a', AKGL_MAX_STRING_LENGTH);
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
|
|
CATCH(errctx, akgl_heap_next_string(&ptr));
|
|
strncpy(ptr->data, (char *)©buf, AKGL_MAX_STRING_LENGTH);
|
|
}
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_akgl_string_initialize(void)
|
|
{
|
|
akgl_String *ptr;
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&ptr));
|
|
CATCH(errctx, akgl_string_initialize(ptr, NULL));
|
|
FAIL_NONZERO_BREAK(errctx, ptr->data[0], AKERR_VALUE, "Expected empty zero length string data");
|
|
|
|
CATCH(errctx, akgl_heap_release_string(ptr));
|
|
CATCH(errctx, akgl_heap_next_string(&ptr));
|
|
CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
|
|
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
|
|
|
|
CATCH(errctx, akgl_heap_release_string(NULL));
|
|
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
|
|
void reset_string_heap(void)
|
|
{
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief akgl_string_initialize must not write past the buffer it is zeroing.
|
|
*
|
|
* The `NULL` init path zeroed `sizeof(akgl_String)` bytes starting at `data`.
|
|
* `data` begins after the `int refcount` in front of it, so that ran four bytes
|
|
* past the end of the object -- straight onto the *next* pool slot's refcount,
|
|
* which is the field the allocator uses to decide whether a slot is free.
|
|
*
|
|
* Claiming two adjacent slots and initializing the first is enough to catch it:
|
|
* the second's refcount goes to zero and the pool believes it is free while the
|
|
* caller is still holding it.
|
|
*/
|
|
akerr_ErrorContext *test_akgl_string_initialize_stays_in_bounds(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_String *first = NULL;
|
|
akgl_String *second = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&first));
|
|
CATCH(errctx, akgl_heap_next_string(&second));
|
|
TEST_ASSERT(errctx, second == (first + 1),
|
|
"the pool did not hand out adjacent slots; this test needs them");
|
|
|
|
// A sentinel the overrun would land on. akgl_heap_next_string has
|
|
// already set it to 1; make it something an accidental write cannot
|
|
// coincide with.
|
|
second->refcount = 0x5A;
|
|
|
|
CATCH(errctx, akgl_string_initialize(first, NULL));
|
|
|
|
TEST_ASSERT(errctx, second->refcount == 0x5A,
|
|
"initializing a string wrote past its buffer: the next slot's "
|
|
"refcount is %d, expected %d",
|
|
second->refcount, 0x5A);
|
|
TEST_ASSERT(errctx, first->refcount == 1,
|
|
"initializing a string left its own refcount at %d, expected 1",
|
|
first->refcount);
|
|
TEST_ASSERT(errctx, first->data[0] == '\0',
|
|
"initializing a string with NULL did not zero its buffer");
|
|
TEST_ASSERT(errctx, first->data[AKGL_MAX_STRING_LENGTH - 1] == '\0',
|
|
"initializing a string with NULL did not zero its last byte");
|
|
} CLEANUP {
|
|
if ( second != NULL ) {
|
|
second->refcount = 1;
|
|
IGNORE(akgl_heap_release_string(second));
|
|
}
|
|
if ( first != NULL ) {
|
|
IGNORE(akgl_heap_release_string(first));
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief akgl_string_copy must refuse a count that would leave both buffers.
|
|
*
|
|
* Both slots are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count read
|
|
* past the end of one and wrote past the end of the other. The header used to
|
|
* document that as behaviour.
|
|
*/
|
|
akerr_ErrorContext *test_akgl_string_copy_bounds_its_count(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_String *src = NULL;
|
|
akgl_String *dest = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&src));
|
|
CATCH(errctx, akgl_heap_next_string(&dest));
|
|
CATCH(errctx, akgl_string_initialize(src, "bounded"));
|
|
CATCH(errctx, akgl_string_initialize(dest, NULL));
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH + 1),
|
|
"copying one byte more than a pool string holds");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_string_copy(src, dest, -1),
|
|
"copying a negative number of bytes");
|
|
|
|
// The boundary itself is legal: it is exactly the buffer.
|
|
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH),
|
|
"copying exactly a pool string's length");
|
|
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
|
|
"a full-length copy did not transfer the contents");
|
|
|
|
// 0 still means "the whole buffer" rather than "nothing".
|
|
CATCH(errctx, akgl_string_initialize(dest, NULL));
|
|
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, 0),
|
|
"copying with a count of zero");
|
|
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
|
|
"a zero-count copy did not transfer the whole buffer");
|
|
} CLEANUP {
|
|
if ( dest != NULL ) {
|
|
IGNORE(akgl_heap_release_string(dest));
|
|
}
|
|
if ( src != NULL ) {
|
|
IGNORE(akgl_heap_release_string(src));
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
TEST_TRAP_UNHANDLED_ERRORS();
|
|
printf("test_fresh_heap_gives_string ....\n");
|
|
test_fresh_heap_gives_strings();
|
|
reset_string_heap();
|
|
printf("test_string_heap_error_when_no_strings_left ...\n");
|
|
test_string_heap_error_when_no_strings_left();
|
|
reset_string_heap();
|
|
printf("test_string_heap_honors_refcount ...\n");
|
|
test_string_heap_honors_refcount();
|
|
reset_string_heap();
|
|
printf("test_strcpy_to_all_strings_no_segfault ...\n");
|
|
test_strcpy_to_all_strings_no_segfault();
|
|
reset_string_heap();
|
|
printf("test_akgl_string_initialize....\n");
|
|
test_akgl_string_initialize();
|
|
reset_string_heap();
|
|
printf("test_akgl_string_initialize_stays_in_bounds ...\n");
|
|
CATCH(errctx, test_akgl_string_initialize_stays_in_bounds());
|
|
reset_string_heap();
|
|
printf("test_akgl_string_copy_bounds_its_count ...\n");
|
|
CATCH(errctx, test_akgl_string_copy_bounds_its_count());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
return 0;
|
|
}
|