Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
429 lines
13 KiB
C
429 lines
13 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;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
/*
|
|
* 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_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);
|
|
}
|