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>
75 lines
3.6 KiB
C
75 lines
3.6 KiB
C
/**
|
|
* @file staticstring.h
|
|
* @brief A fixed-capacity string object handed out by the akgl string heap layer.
|
|
*
|
|
* The library allocates nothing at runtime, so a "string" here is a
|
|
* PATH_MAX-sized buffer claimed from the pool with akgl_heap_next_string and
|
|
* given back with akgl_heap_release_string. Capacity is fixed at compile time:
|
|
* these functions truncate rather than grow, and truncation is silent.
|
|
*/
|
|
|
|
#ifndef _AKGL_STATICSTRING_H_
|
|
#define _AKGL_STATICSTRING_H_
|
|
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
#include <limits.h>
|
|
|
|
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
|
|
|
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
|
typedef struct
|
|
{
|
|
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
|
|
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
|
|
} akgl_String;
|
|
|
|
/**
|
|
* @brief Set a pooled string's contents and mark the slot in use.
|
|
*
|
|
* Copies at most #AKGL_MAX_STRING_LENGTH bytes out of @p init, or zeroes the
|
|
* buffer when @p init is `NULL`, then sets `refcount` to 1. Callers normally
|
|
* reach this through akgl_heap_next_string rather than calling it directly.
|
|
*
|
|
* @param obj The pooled string to (re)initialize. Required. Its previous
|
|
* contents are discarded without inspection.
|
|
* @param init Initial contents, NUL-terminated. Optional -- `NULL` zero-fills
|
|
* the buffer instead. An @p init longer than
|
|
* #AKGL_MAX_STRING_LENGTH is truncated *and left unterminated*,
|
|
* because this is `strncpy` semantics, not `strlcpy`.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
|
*
|
|
* @note Until 0.5.0 the `NULL` @p init path zeroed `sizeof(akgl_String)` bytes
|
|
* starting at `data`, which is four bytes past the end of the buffer --
|
|
* `refcount` sits in front of it, so the overrun landed on the next pool
|
|
* slot's reference count.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
|
/**
|
|
* @brief Copy the contents of one pooled string into another.
|
|
*
|
|
* A bounded `strncpy` between two already-claimed pool slots. It copies bytes
|
|
* only: `refcount` is left alone, so @p dest keeps whatever pool state it had.
|
|
*
|
|
* @param src Source string. Required. Read up to @p count bytes.
|
|
* @param dest Destination string. Required. Overwritten in place; the pool
|
|
* slot must already have been claimed.
|
|
* @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the
|
|
* whole buffer. A @p count shorter than the source truncates
|
|
* without writing a terminator; a @p count longer than the source
|
|
* zero-pads the remainder, per `strncpy`. A negative @p count, or
|
|
* one above #AKGL_MAX_STRING_LENGTH, is refused -- both buffers
|
|
* are exactly that long, so a larger count walked off the end of
|
|
* two pool slots at once.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p src or @p dest is `NULL`.
|
|
* @throws AKERR_OUTOFBOUNDS If @p count is negative or above
|
|
* #AKGL_MAX_STRING_LENGTH.
|
|
* @throws errno Whatever `errno` holds if `strncpy` returns something other
|
|
* than @p dest. In practice `strncpy` always returns its destination, so
|
|
* this path is unreachable rather than merely rare.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dest, int count);
|
|
#endif //_AKGL_STATICSTRING_H_
|