From: Simon Glass <simon.glass@canonical.com> We know or assume that dlmalloc itself works correctly, but there is still the possibility that the U-Boot integration has bugs. Add a test suite for the malloc() implementation, covering: - Basic malloc/free operations - Edge cases (zero size, NULL pointer handling) - realloc() in various scenarios - memalign() with different alignments - Multiple allocations and fragmentation - malloc_enable_testing() failure simulation - Large allocations (1MB, 16MB) - Full pool allocation (CONFIG_SYS_MALLOC_LEN plus environment size) - Fill pool test with random sizes Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/common/Makefile | 1 + test/common/malloc.c | 629 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 630 insertions(+) create mode 100644 test/common/malloc.c diff --git a/test/common/Makefile b/test/common/Makefile index a5df946396a..9674bbec030 100644 --- a/test/common/Makefile +++ b/test/common/Makefile @@ -13,5 +13,6 @@ obj-$(CONFIG_CONSOLE_PAGER) += console.o obj-$(CONFIG_CYCLIC) += cyclic.o obj-$(CONFIG_EVENT_DYNAMIC) += event.o obj-y += cread.o +obj-y += malloc.o obj-$(CONFIG_CONSOLE_PAGER) += pager.o obj-$(CONFIG_$(PHASE_)CMDLINE) += print.o diff --git a/test/common/malloc.c b/test/common/malloc.c new file mode 100644 index 00000000000..b114267dd83 --- /dev/null +++ b/test/common/malloc.c @@ -0,0 +1,629 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Tests for malloc() implementation + * + * Copyright 2025 Google LLC + * Written by Simon Glass <sjg@chromium.org> + */ + +#include <linux/sizes.h> +#include <malloc.h> +#include <mapmem.h> +#include <stdlib.h> +#include <asm/global_data.h> +#include <env_internal.h> +#include <test/common.h> +#include <test/test.h> +#include <test/ut.h> + +DECLARE_GLOBAL_DATA_PTR; + +/* + * get_alloced_size() - Get currently allocated memory size + * + * Return: Number of bytes currently allocated (not freed) + */ +static int get_alloced_size(void) +{ + struct mallinfo info = mallinfo(); + + return info.uordblks; +} + +/* Test basic malloc() and free() */ +static int common_test_malloc_basic(struct unit_test_state *uts) +{ + int before; + void *ptr; + + before = get_alloced_size(); + + ptr = malloc(100); + ut_assertnonnull(ptr); + + ut_assert(get_alloced_size() >= before + 100); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_basic, 0); + +/* Test malloc() with zero size and free(NULL) */ +static int common_test_malloc_zero(struct unit_test_state *uts) +{ + int before; + void *ptr; + + before = get_alloced_size(); + + ptr = malloc(0); + ut_assertnonnull(ptr); + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_zero, 0); + +/* Test calloc() zeros memory */ +static int common_test_calloc(struct unit_test_state *uts) +{ + int before, i; + char *ptr; + + before = get_alloced_size(); + + ptr = calloc(100, 1); + ut_assertnonnull(ptr); + + for (i = 0; i < 100; i++) + ut_asserteq(0, ptr[i]); + + ut_assert(get_alloced_size() >= before + 100); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_calloc, 0); + +/* Test realloc() to larger size */ +static int common_test_realloc_larger(struct unit_test_state *uts) +{ + char *ptr, *ptr2; + int before, i; + + before = get_alloced_size(); + + ptr = malloc(50); + ut_assertnonnull(ptr); + + for (i = 0; i < 50; i++) + ptr[i] = i; + + ptr2 = realloc(ptr, 100); + ut_assertnonnull(ptr2); + + /* + * Check original data preserved + */ + for (i = 0; i < 50; i++) + ut_asserteq(i, ptr2[i]); + + free(ptr2); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_realloc_larger, 0); + +/* Test realloc() to smaller size */ +static int common_test_realloc_smaller(struct unit_test_state *uts) +{ + char *ptr, *ptr2; + int before, i; + + before = get_alloced_size(); + + ptr = malloc(100); + ut_assertnonnull(ptr); + + for (i = 0; i < 100; i++) + ptr[i] = i; + + ptr2 = realloc(ptr, 50); + ut_assertnonnull(ptr2); + + /* + * Check data preserved + */ + for (i = 0; i < 50; i++) + ut_asserteq(i, ptr2[i]); + + free(ptr2); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_realloc_smaller, 0); + +/* Test realloc() with NULL pointer (should act like malloc) */ +static int common_test_realloc_null(struct unit_test_state *uts) +{ + int before; + void *ptr; + + before = get_alloced_size(); + + ptr = realloc(NULL, 100); + ut_assertnonnull(ptr); + ut_assert(get_alloced_size() >= before + 100); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_realloc_null, 0); + +/* + * Test realloc() with zero size + * + * Standard dlmalloc behavior (without REALLOC_ZERO_BYTES_FREES or mcheck): + * realloc(ptr, 0) returns a minimum-sized allocation. + */ +static int common_test_realloc_zero(struct unit_test_state *uts) +{ + void *ptr, *ptr2; + int before; + + before = get_alloced_size(); + + ptr = malloc(100); + ut_assertnonnull(ptr); + ut_assert(get_alloced_size() >= before + 100); + + ptr2 = realloc(ptr, 0); + + /* + * dlmalloc returns a minimum-sized allocation for realloc(ptr, 0) + * since REALLOC_ZERO_BYTES_FREES is not enabled. + * It may realloc in-place or return a different pointer. + */ + ut_assertnonnull(ptr2); + + free(ptr2); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_realloc_zero, 0); + +/* Test memalign() with various alignments */ +static int common_test_memalign(struct unit_test_state *uts) +{ + int before; + void *ptr; + + before = get_alloced_size(); + + /* + * Test power-of-2 alignments + */ + ptr = memalign(16, 100); + ut_assertnonnull(ptr); + ut_asserteq(0, (ulong)ptr & 0xf); + free(ptr); + + ptr = memalign(256, 100); + ut_assertnonnull(ptr); + ut_asserteq(0, (ulong)ptr & 0xff); + free(ptr); + + ptr = memalign(4096, 100); + ut_assertnonnull(ptr); + ut_asserteq(0, (ulong)ptr & 0xfff); + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_memalign, 0); + +/* Test multiple allocations */ +static int common_test_malloc_multiple(struct unit_test_state *uts) +{ + int expected = 0, before, i; + void *ptrs[10]; + + before = get_alloced_size(); + + /* Allocate multiple blocks */ + for (i = 0; i < 10; i++) { + ptrs[i] = malloc((i + 1) * 100); + ut_assertnonnull(ptrs[i]); + expected += (i + 1) * 100; + } + + /* Should have allocated at least the requested amount */ + ut_assert(get_alloced_size() >= before + expected); + + /* Free in reverse order */ + for (i = 9; i >= 0; i--) + free(ptrs[i]); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_multiple, 0); + +/* Test malloc() failure when testing enabled */ +static int common_test_malloc_failure(struct unit_test_state *uts) +{ + void *ptr1, *ptr2, *ptr3; + int before; + + before = get_alloced_size(); + + /* Enable failure after 2 allocations */ + malloc_enable_testing(2); + + ptr1 = malloc(100); + ut_assertnonnull(ptr1); + + ptr2 = malloc(100); + ut_assertnonnull(ptr2); + + /* This should fail */ + ptr3 = malloc(100); + ut_assertnull(ptr3); + + malloc_disable_testing(); + + /* Should work again */ + ptr3 = malloc(100); + ut_assertnonnull(ptr3); + + free(ptr1); + free(ptr2); + free(ptr3); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_failure, 0); + +/* Test realloc() failure when testing enabled */ +static int common_test_realloc_failure(struct unit_test_state *uts) +{ + void *ptr1, *ptr2; + int before; + + before = get_alloced_size(); + + ptr1 = malloc(50); + ut_assertnonnull(ptr1); + + /* Enable failure after 0 allocations */ + malloc_enable_testing(0); + + /* This should fail and return NULL, leaving ptr1 intact */ + ptr2 = realloc(ptr1, 100); + ut_assertnull(ptr2); + + malloc_disable_testing(); + + /* ptr1 should still be valid, try to realloc it */ + ptr2 = realloc(ptr1, 100); + ut_assertnonnull(ptr2); + + free(ptr2); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_realloc_failure, 0); + +/* Test large allocation */ +static int common_test_malloc_large(struct unit_test_state *uts) +{ + int size = SZ_1M, before; + void *ptr; + + before = get_alloced_size(); + + ptr = malloc(size); + ut_assertnonnull(ptr); + memset(ptr, 0x5a, size); + + ut_assert(get_alloced_size() >= before + size); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_large, 0); + +/* Test many small allocations (tests binning) */ +static int common_test_malloc_small_bins(struct unit_test_state *uts) +{ + int after_free, before, i; + void *ptrs[100]; + + before = get_alloced_size(); + + /* Allocate many small blocks of various sizes */ + for (i = 0; i < 100; i++) { + ptrs[i] = malloc((i % 32) + 8); + ut_assertnonnull(ptrs[i]); + } + + /* Free every other one to create fragmentation */ + for (i = 0; i < 100; i += 2) + free(ptrs[i]); + + after_free = get_alloced_size(); + + /* Allocate more to test reuse */ + for (i = 0; i < 100; i += 2) { + ptrs[i] = malloc((i % 32) + 8); + ut_assertnonnull(ptrs[i]); + } + + /* Should be back to roughly the same size (may vary due to overhead) */ + ut_assert(get_alloced_size() >= after_free); + + /* Free all */ + for (i = 0; i < 100; i++) + free(ptrs[i]); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_small_bins, 0); + +/* Test alternating allocation sizes */ +static int common_test_malloc_alternating(struct unit_test_state *uts) +{ + void *small1, *large1, *small2, *large2; + int before; + + before = get_alloced_size(); + + small1 = malloc(32); + ut_assertnonnull(small1); + + large1 = malloc(8192); + ut_assertnonnull(large1); + + small2 = malloc(64); + ut_assertnonnull(small2); + + large2 = malloc(16384); + ut_assertnonnull(large2); + + ut_assert(get_alloced_size() >= before + 32 + 8192 + 64 + 16384); + + free(small1); + free(large1); + free(small2); + free(large2); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_alternating, 0); + +/* Test malloc() with boundary sizes */ +static int common_test_malloc_boundaries(struct unit_test_state *uts) +{ + int before; + void *ptr; + + before = get_alloced_size(); + + /* Test allocation right at small/large boundary (typically 256 bytes) */ + ptr = malloc(256); + ut_assertnonnull(ptr); + free(ptr); + + ptr = malloc(255); + ut_assertnonnull(ptr); + free(ptr); + + ptr = malloc(257); + ut_assertnonnull(ptr); + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_boundaries, 0); + +/* Test malloc_usable_size() */ +static int common_test_malloc_usable_size(struct unit_test_state *uts) +{ + int before, size; + void *ptr; + + before = get_alloced_size(); + + ptr = malloc(100); + ut_assertnonnull(ptr); + + size = malloc_usable_size(ptr); + /* Usable size should be at least the requested size */ + ut_assert(size >= 100); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_usable_size, 0); + +/* Test mallinfo() returns reasonable values */ +static int common_test_mallinfo(struct unit_test_state *uts) +{ + void *ptr1, *ptr2, *ptr3; + struct mallinfo info; + int arena_before; + int used_after1; + int used_after2; + int before; + + before = get_alloced_size(); + + info = mallinfo(); + arena_before = info.arena; + + ptr1 = malloc(1024); + ut_assertnonnull(ptr1); + + info = mallinfo(); + /* Arena size should not change (it's the total heap size) */ + ut_asserteq(arena_before, info.arena); + /* Used memory should increase */ + ut_assert(info.uordblks >= before + 1024); + used_after1 = info.uordblks; + + ptr2 = malloc(2048); + ut_assertnonnull(ptr2); + + info = mallinfo(); + ut_asserteq(arena_before, info.arena); + ut_assert(info.uordblks >= used_after1 + 2048); + used_after2 = info.uordblks; + + ptr3 = malloc(512); + ut_assertnonnull(ptr3); + + info = mallinfo(); + ut_asserteq(arena_before, info.arena); + ut_assert(info.uordblks >= used_after2 + 512); + + free(ptr1); + free(ptr2); + free(ptr3); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_mallinfo, 0); + +/* Test allocating a very large size */ +static int common_test_malloc_very_large(struct unit_test_state *uts) +{ + size_t size, before; + void *ptr; + + before = get_alloced_size(); + size = TOTAL_MALLOC_LEN - before - SZ_64K; + + ptr = malloc(size); + ut_assertnonnull(ptr); + ut_assert(get_alloced_size() >= before + size); + + free(ptr); + + ut_asserteq(before, get_alloced_size()); + + return 0; +} +COMMON_TEST(common_test_malloc_very_large, 0); + +/* Test allocating the full malloc pool size */ +static int common_test_malloc_full_pool(struct unit_test_state *uts) +{ + /* Try to allocate the full pool size - should fail due to overhead */ + ut_assertnull(malloc(TOTAL_MALLOC_LEN)); + + return 0; +} +COMMON_TEST(common_test_malloc_full_pool, 0); + +/* Test filling the entire malloc pool with allocations */ +static int common_test_malloc_fill_pool(struct unit_test_state *uts) +{ + int alloc_size, before, count, i, total; + const int ptr_table_size = 0x100000; + void **ptrs; + void *ptr; + + /* + * this is only really safe on sandbox since it uses up all memory and + * assumed that at least half of the malloc() pool is unallocated + */ + if (!IS_ENABLED(CONFIG_SANDBOX)) + return -EAGAIN; + + before = get_alloced_size(); + + /* Use memory outside malloc pool to store pointers */ + ptrs = map_sysmem(0x1000, ptr_table_size); + + /* Allocate until we run out of memory, using random sizes */ + count = 0; + total = 0; + while (1) { + /* Random size up to 1 MB */ + alloc_size = rand() % (SZ_1M); + ptr = malloc(alloc_size); + if (!ptr) + break; + ptrs[count++] = ptr; + total += alloc_size; + /* Safety check to avoid infinite loop */ + if (count >= ptr_table_size / sizeof(void *)) + break; + } + printf("count %d total %d ptr_table_size %d\n", count, total, + ptr_table_size); + + /* + * Should have allocated most of the pool - if we can't allocate + * 1MB, then at most 1MB is available, so we must have allocated + * at least (pool_size - 1MB) + */ + ut_assert(count > 0); + ut_assert(count < ptr_table_size / sizeof(void *)); + ut_assert(get_alloced_size() >= TOTAL_MALLOC_LEN - SZ_1M); + + /* Free all allocations */ + for (i = 0; i < count; i++) + free(ptrs[i]); + + /* Should be back to starting state */ + ut_asserteq(before, get_alloced_size()); + + /* Verify we can allocate large blocks again */ + ptr = malloc(TOTAL_MALLOC_LEN / 2); + ut_assertnonnull(ptr); + free(ptr); + + unmap_sysmem(ptrs); + + return 0; +} +COMMON_TEST(common_test_malloc_fill_pool, 0); -- 2.43.0