Files
libakstdlib/tests/test_memory.c
Tachikoma 55d986c631
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 5m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 12m39s
Drop the TODO.md section numbers from 26 files
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a
numbering the file had already abandoned before the move to the tracker. They
label completed work, so the pointer was the only wrong part.

The citation is removed and the sentence kept, which is what issue #27
recommended: these are labels, not references, and a label carrying a
version-dependent pointer goes stale again at the next reorganisation. Where a
pointer earns its place it names what actually holds the content now --
UPGRADING.md for the confirmed defects, libakerror #15 for the target
namespacing, issue #7 for the mutation survivors.

README.md and akstdlib.h sent readers to TODO.md for 'what is still open';
they name the tracker.

Verified: cmake --build build && ctest --test-dir build, 19/19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:27 -04:00

429 lines
13 KiB
C

/*
* Memory wrappers, complete, plus the additions from
* section 3.1.
*
* The three cases the wrapper plan left open are all pinned here: malloc(0) is AKERR_VALUE
* rather than whatever errno happened to hold when the platform's malloc(0)
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
* AKERR_VALUE rather than left as undefined behaviour that usually looks fine.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <stdint.h>
static int test_malloc_writes_nonnull_pointer(void)
{
void *ptr = NULL;
AKSL_CHECK_OK(aksl_malloc(32, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK_OK(aksl_free(ptr));
return 0;
}
static int test_malloc_rejects_null_destination(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(32, NULL),
AKERR_NULLPOINTER, "dst=");
return 0;
}
/*
* The wrapper plan asked for this contract to be pinned down. malloc(0) is allowed to
* return either a unique pointer or NULL, and a NULL there is not a failure and
* need not set errno -- so the old wrapper could raise an error whose status was
* 0, which every DETECT downstream reads as success while the context holds a
* pool slot. A zero-size request is refused on its own terms instead.
*/
static int test_malloc_rejects_zero_size(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(0, &ptr), AKERR_VALUE, "zero-size");
AKSL_CHECK(ptr == NULL);
return 0;
}
/*
* An allocation no system can satisfy. SIZE_MAX/2 rather than SIZE_MAX because
* some allocators reject the latter as an obvious overflow before ever
* consulting the system, and the case worth testing is the one where the request
* is plausible and the answer is still no.
*/
static int test_malloc_reports_out_of_memory(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_STATUS(aksl_malloc(SIZE_MAX / 2, &ptr), ENOMEM);
/* Never garbage on the failure path. */
AKSL_CHECK(ptr == NULL);
return 0;
}
static int test_calloc_zeroes_and_guards(void)
{
unsigned char *buf = NULL;
void *ptr = (void *)0x1;
size_t i = 0;
AKSL_CHECK_OK(aksl_calloc(16, sizeof(unsigned char), (void **)&buf));
AKSL_CHECK(buf != NULL);
for ( i = 0; i < 16; i++ ) {
AKSL_CHECK(buf[i] == 0);
}
AKSL_CHECK_OK(aksl_free(buf));
AKSL_CHECK_STATUS(aksl_calloc(16, 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_calloc(1, 0, &ptr), AKERR_VALUE);
return 0;
}
/*
* realloc(3)'s trap: on failure it returns NULL and the original block is *still
* valid*, so `p = realloc(p, n)` leaks it. The wrapper takes the pointer by
* reference and leaves it untouched on failure, so the caller still holds a
* pointer it can free -- which is what this asserts by freeing it.
*/
static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
{
unsigned char *buf = NULL;
void *ptr = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(8, (void **)&buf));
for ( i = 0; i < 8; i++ ) {
buf[i] = (unsigned char)i;
}
ptr = buf;
AKSL_CHECK_OK(aksl_realloc(&ptr, 64));
buf = (unsigned char *)ptr;
AKSL_CHECK(buf != NULL);
/* The contents that fit are carried over. */
for ( i = 0; i < 8; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
/* A request that cannot be met leaves ptr aimed at the original block. */
AKSL_CHECK_STATUS(aksl_realloc(&ptr, SIZE_MAX / 2), ENOMEM);
AKSL_CHECK(ptr == (void *)buf);
for ( i = 0; i < 8; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
AKSL_CHECK_OK(aksl_free(ptr));
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
return 0;
}
/*
* reallocarray's whole reason for existing: `realloc(p, n * size)` is a heap
* overflow waiting for an n large enough to wrap, and the multiplication is the
* place a caller does not think to look.
*/
static int test_reallocarray_checks_the_multiplication(void)
{
void *ptr = NULL;
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(4, &ptr));
AKSL_CHECK_OK(aksl_reallocarray(&ptr, 16, 8));
buf = (unsigned char *)ptr;
for ( i = 0; i < 128; i++ ) {
buf[i] = (unsigned char)i;
}
AKSL_CHECK_OK(aksl_free(ptr));
ptr = NULL;
/* SIZE_MAX/4 members of 8 bytes wraps; without the check this would be a
* plausible-looking small allocation followed by a very large write. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_reallocarray(&ptr, SIZE_MAX / 4, 8),
AKERR_OUTOFBOUNDS, "overflows size_t");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_reallocarray(NULL, 1, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 0, 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 1, 0), AKERR_VALUE);
return 0;
}
/*
* aligned_alloc(3) requires size to be a multiple of the alignment and the
* alignment to be a power of two. Violating either is undefined behaviour that
* usually just returns NULL, so both are checked and reported as what they are.
*/
static int test_aligned_alloc_checks_its_preconditions(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_aligned_alloc(64, 128, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % 64) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(24, 48, &ptr),
AKERR_VALUE, "not a power of two");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(64, 100, &ptr),
AKERR_VALUE, "not a multiple of alignment");
AKSL_CHECK_STATUS(aksl_aligned_alloc(0, 64, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 128, NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* posix_memalign(3) breaks the errno convention: it returns the error number and
* leaves errno alone. The wrapper reports it as a status like everything else.
*/
static int test_posix_memalign(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_posix_memalign(&ptr, sizeof(void *) * 2, 100));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % (sizeof(void *) * 2)) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
/* Not a multiple of sizeof(void *), which posix_memalign refuses. */
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 3, 100), EINVAL);
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_posix_memalign(NULL, 16, 100), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 16, 0), AKERR_VALUE);
return 0;
}
static int test_free_rejects_null_pointer(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
AKERR_NULLPOINTER, "NULL");
return 0;
}
/* aksl_freep clears the caller's pointer, so a second free is caught. */
static int test_freep_clears_the_pointer(void)
{
void *ptr = NULL;
AKSL_CHECK_OK(aksl_malloc(16, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK_OK(aksl_freep(&ptr));
AKSL_CHECK(ptr == NULL);
/* The second attempt is now an error instead of a double free. */
AKSL_CHECK_STATUS(aksl_freep(&ptr), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_malloc_free_round_trip(void)
{
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(64, (void **)&buf));
AKSL_CHECK(buf != NULL);
for ( i = 0; i < 64; i++ ) {
buf[i] = (unsigned char)i;
}
for ( i = 0; i < 64; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
AKSL_CHECK_OK(aksl_free(buf));
return 0;
}
static int test_memset_fills_buffer(void)
{
unsigned char buf[8];
size_t i = 0;
memset((void *)buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_memset(buf, 0x5a, sizeof(buf)));
for ( i = 0; i < sizeof(buf); i++ ) {
AKSL_CHECK(buf[i] == 0x5a);
}
return 0;
}
static int test_memset_zero_length_is_noop(void)
{
unsigned char buf[4] = { 1, 2, 3, 4 };
AKSL_CHECK_OK(aksl_memset(buf, 0xff, 0));
AKSL_CHECK(buf[0] == 1);
AKSL_CHECK(buf[1] == 2);
AKSL_CHECK(buf[2] == 3);
AKSL_CHECK(buf[3] == 4);
return 0;
}
static int test_memset_rejects_null_buffer(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memset(NULL, 0x00, 4),
AKERR_NULLPOINTER, "s=");
return 0;
}
static int test_memcpy_copies_bytes(void)
{
unsigned char src[6] = { 0, 1, 2, 3, 4, 5 };
unsigned char dst[6] = { 9, 9, 9, 9, 9, 9 };
size_t i = 0;
AKSL_CHECK_OK(aksl_memcpy(dst, src, sizeof(src)));
for ( i = 0; i < sizeof(src); i++ ) {
AKSL_CHECK(dst[i] == src[i]);
}
return 0;
}
static int test_memcpy_zero_length_is_noop(void)
{
unsigned char src[3] = { 1, 2, 3 };
unsigned char dst[3] = { 4, 5, 6 };
AKSL_CHECK_OK(aksl_memcpy(dst, src, 0));
AKSL_CHECK(dst[0] == 4);
AKSL_CHECK(dst[1] == 5);
AKSL_CHECK(dst[2] == 6);
return 0;
}
/*
* The wrapper plan's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
* it undefined behaviour, which in practice means "works until the day a
* compiler version or a length changes and it does not". Callers who mean to
* overlap want aksl_memmove, and the message says so.
*/
static int test_memcpy_rejects_overlapping_ranges(void)
{
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
/* Destination inside the source range. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memcpy(&buf[2], &buf[0], 4),
AKERR_VALUE, "aksl_memmove");
/* Source inside the destination range. */
AKSL_CHECK_STATUS(aksl_memcpy(&buf[0], &buf[2], 4), AKERR_VALUE);
/* Adjacent but not overlapping is fine. */
AKSL_CHECK_OK(aksl_memcpy(&buf[4], &buf[0], 4));
AKSL_CHECK(buf[4] == 0 && buf[5] == 1 && buf[6] == 2 && buf[7] == 3);
return 0;
}
static int test_memmove_handles_overlap(void)
{
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
AKSL_CHECK_OK(aksl_memmove(&buf[2], &buf[0], 4));
AKSL_CHECK(buf[2] == 0 && buf[3] == 1 && buf[4] == 2 && buf[5] == 3);
AKSL_CHECK_STATUS(aksl_memmove(NULL, buf, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memmove(buf, NULL, 1), AKERR_NULLPOINTER);
return 0;
}
static int test_memcpy_rejects_null_destination(void)
{
unsigned char src[1] = { 1 };
AKSL_CHECK_STATUS(aksl_memcpy(NULL, src, sizeof(src)),
AKERR_NULLPOINTER);
return 0;
}
static int test_memcpy_rejects_null_source(void)
{
unsigned char dst[1] = { 0 };
AKSL_CHECK_STATUS(aksl_memcpy(dst, NULL, sizeof(dst)),
AKERR_NULLPOINTER);
return 0;
}
/*
* memcmp and memchr answer through an out-param, because the return value is
* already spoken for by the error context. A memchr that finds nothing is not an
* error: "absent" is an ordinary answer to that question, so *dest is NULL and
* the call succeeds.
*/
static int test_memcmp_reports_ordering(void)
{
unsigned char a[4] = { 1, 2, 3, 4 };
unsigned char b[4] = { 1, 2, 3, 4 };
unsigned char c[4] = { 1, 2, 9, 4 };
int result = 99;
AKSL_CHECK_OK(aksl_memcmp(a, b, sizeof(a), &result));
AKSL_CHECK(result == 0);
AKSL_CHECK_OK(aksl_memcmp(a, c, sizeof(a), &result));
AKSL_CHECK(result < 0);
AKSL_CHECK_OK(aksl_memcmp(c, a, sizeof(a), &result));
AKSL_CHECK(result > 0);
AKSL_CHECK_STATUS(aksl_memcmp(NULL, b, 4, &result), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(a, NULL, 4, &result), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(a, b, 4, NULL), AKERR_NULLPOINTER);
return 0;
}
static int test_memchr_finds_or_reports_absence(void)
{
unsigned char buf[5] = { 'a', 'b', 'c', 'd', 'e' };
void *found = NULL;
AKSL_CHECK_OK(aksl_memchr(buf, 'c', sizeof(buf), &found));
AKSL_CHECK(found == (void *)&buf[2]);
/* Not found is a successful answer of "nowhere". */
AKSL_CHECK_OK(aksl_memchr(buf, 'z', sizeof(buf), &found));
AKSL_CHECK(found == NULL);
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memchr(buf, 'a', 1, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_malloc_writes_nonnull_pointer);
AKSL_RUN(failures, test_malloc_rejects_null_destination);
AKSL_RUN(failures, test_malloc_rejects_zero_size);
AKSL_RUN(failures, test_malloc_reports_out_of_memory);
AKSL_RUN(failures, test_malloc_free_round_trip);
AKSL_RUN(failures, test_calloc_zeroes_and_guards);
AKSL_RUN(failures, test_reallocarray_checks_the_multiplication);
AKSL_RUN(failures, test_aligned_alloc_checks_its_preconditions);
AKSL_RUN(failures, test_posix_memalign);
AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure);
AKSL_RUN(failures, test_free_rejects_null_pointer);
AKSL_RUN(failures, test_freep_clears_the_pointer);
AKSL_RUN(failures, test_memset_fills_buffer);
AKSL_RUN(failures, test_memset_zero_length_is_noop);
AKSL_RUN(failures, test_memset_rejects_null_buffer);
AKSL_RUN(failures, test_memcpy_copies_bytes);
AKSL_RUN(failures, test_memcpy_zero_length_is_noop);
AKSL_RUN(failures, test_memcpy_rejects_overlapping_ranges);
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
AKSL_RUN(failures, test_memcpy_rejects_null_source);
AKSL_RUN(failures, test_memmove_handles_overlap);
AKSL_RUN(failures, test_memcmp_reports_ordering);
AKSL_RUN(failures, test_memchr_finds_or_reports_absence);
AKSL_REPORT(failures);
}