/* * 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 #include 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); }