TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.
The defects:
2.1.1 aksl_list_append conflated Floyd cycle detection with finding
the tail, so `tail` tracked the node behind the midpoint. Any
append to a list of 2+ nodes silently dropped everything after
it. Two separate walks now: Floyd to prove the list is finite,
then a plain walk to the end.
2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor,
so the whole first half of the list -- head included -- was
never passed to the callback. It starts at the head.
2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
that raised it handled it and returned success, so the parent
carried on into the sibling subtree. The recursion is split out
and propagates the break; only the public entry swallows it.
2.1.4 va_end now matches every va_start on every path.
2.1.5 The ato* family had no error channel at all. Reimplemented over
a new strto* family with errno cleared, an endptr check and a
range check: AKERR_VALUE for junk, ERANGE for overflow.
2.1.6 aksl_realpath never checked resolved_path, could not be told
the buffer size, and formatted an unspecified buffer with %s on
its own error path. It takes a length; aksl_realpath_alloc is
the allocating form.
The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.
Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.
Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
348 lines
10 KiB
C
348 lines
10 KiB
C
/*
|
|
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
|
|
* section 3.1.
|
|
*
|
|
* The three cases 1.1 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;
|
|
}
|
|
|
|
/*
|
|
* TODO.md 1.1 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* TODO.md 1.1'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_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);
|
|
}
|