/* * vlibc — malloc/free/calloc/realloc/aligned_alloc/posix_memalign test * (todo 7). * * Exercises the heap allocator end to end: * * 1. malloc(1) / malloc(1KB) / malloc(1MB) / malloc(1GB) — the 1GB case * takes the mmap path — all non-NULL, 16-byte aligned, distinct, * writable; freed again. * 2. malloc(0) returns a unique usable minimum-size block; two calls * yield distinct pointers. * 3. calloc(100, 100) returns 10000 zero bytes; a zero-product calloc * returns a unique zeroed block. * 4. realloc grows (contents preserved), shrinks in place, realloc(p, 0) * frees and returns NULL, realloc(NULL, n) behaves as malloc(n). * 5. aligned_alloc(64, 4096) and aligned_alloc(256, 8192) return * correctly aligned usable blocks; free works on them. * 6. posix_memalign(&p, 16, 100) returns 0 with p 16-aligned; the * negative case posix_memalign(&p, 3, 8) returns EINVAL and leaves p * unmodified. * 7. malloc_usable_size reports at least the requested size (level 2). * 8. A 10k random alloc/free churn (sizes up to 4 KiB plus occasional * 256 KiB mmap blocks) ends with the allocator's own walk * (__vlibc_malloc_check) reporting zero live chunks — the no-leak * proof, no valgrind involved. * * The failure scenarios (allocation overflow, calloc overflow) live in the * `-f` mode: those paths set errno inside the library, and under the host * libc the TCB slot our errno macro addresses is glibc's private TLS state * (writing it corrupts the host; see tests/syscall_test.c). The -f mode * therefore exits through a raw SYS_exit_group without ever touching the * host libc's atexit/cleanup machinery, and the default mode never invokes * those paths at all. The default mode's negative case is posix_memalign, * whose EINVAL is a RETURN VALUE, never an errno write. * * All diagnostics go through raw SYS_write (no stdio): under -Iinclude the * vlibc public headers shadow GCC's internal ones, so a host * would not compile. The stdlib.h below is vlibc's own new header. * * Not part of the library proper; compiled manually for this todo (the * tests/ + make check wiring is owned by a later todo). */ #include #include #include #include "../include/stdlib.h" #include "../src/internal/syscall.h" /* * Internal no-leak hook from src/malloc/malloc.c (hidden, so it never * leaves the archive as a dynamic symbol). Returns the number of live * blocks, or (size_t)-1 when the heap walk disagrees with the counters. */ __attribute__((visibility("hidden"))) size_t __vlibc_malloc_check(void); // NOLINT(bugprone-reserved-identifier) static int failures; /* Write a NUL-terminated string to fd via the raw syscall layer. */ static void say(int fd, const char *s) { long n = 0; while (s[n] != '\0') { n++; } __syscall3(SYS_write, fd, (long)s, n); } /* Write v in decimal to fd. */ static void say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters) { char buf[24]; int i = (int)sizeof(buf); buf[--i] = '\0'; do { buf[--i] = (char)('0' + (v % 10)); v /= 10; } while (v != 0); __syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i)); } static void check(int cond, const char *what) { if (cond) { say(1, "PASS: "); say(1, what); say(1, "\n"); } else { say(2, "FAIL: "); say(2, what); say(2, "\n"); failures++; } } /* xorshift32; deterministic, allocation-independent. */ static unsigned rng_next(unsigned *state) { unsigned x = *state; x ^= x << 13; x ^= x >> 17; x ^= x << 5; *state = x; return x; } /* * Allocation-family calls through noipa proxies so the compiler never sees * constant arguments (a literal 0 product or a folded overflow would trip * the alloc_size diagnostics — or, worse, let GCC treat the call as * alloc_size-undefined and elide it, assuming a non-NULL result). noipa is * required: plain noinline is defeated by same-TU interprocedural * constant propagation. */ static __attribute__((noipa)) void * malloc_proxy(size_t size) { return malloc(size); } static __attribute__((noipa)) void * calloc_proxy(size_t nmemb, size_t size) { return calloc(nmemb, size); } static __attribute__((noipa)) void * realloc_proxy(void *ptr, size_t size) { return realloc(ptr, size); } /* 1. Plain allocations across four orders of magnitude, then free. */ static void basic_alloc_free(void) { static const size_t sizes[] = {1, 1024, 1048576, 1073741824}; void *p[4]; unsigned i; for (i = 0; i < 4; i++) { p[i] = malloc(sizes[i]); check(p[i] != NULL, "malloc returns non-NULL"); check(((uintptr_t)p[i] & 15) == 0, "malloc result is 16-byte aligned"); if (p[i] != NULL) { ((unsigned char *)p[i])[0] = 0x5a; ((unsigned char *)p[i])[sizes[i] - 1] = 0xa5; } } check(p[0] != p[1] && p[1] != p[2] && p[2] != p[3], "distinct blocks for distinct sizes"); for (i = 0; i < 4; i++) { free(p[i]); } check(__vlibc_malloc_check() == 0, "no live chunks after freeing all four"); } /* 2. malloc(0) semantics: unique, usable, minimum-size. */ static void zero_size_malloc(void) { void *a = malloc(0); void *b = malloc(0); check(a != NULL && b != NULL, "malloc(0) returns non-NULL"); check(a != b, "two malloc(0) calls return distinct pointers"); if (a != NULL) { ((unsigned char *)a)[0] = 0x11; } free(a); free(b); } /* 3. calloc zeroing, including a zero product. */ static void calloc_zeroing(void) { unsigned char *p = calloc(100, 100); size_t i; int all_zero = 1; check(p != NULL, "calloc(100, 100) returns non-NULL"); if (p != NULL) { for (i = 0; i < 10000; i++) { if (p[i] != 0) { all_zero = 0; } } } check(all_zero, "calloc(100, 100) is 10000 zero bytes"); free(p); p = calloc_proxy(0, 1); check(p != NULL, "calloc(0, 1) returns a unique zeroed block"); free(p); } /* 4. realloc grow / shrink / free-on-zero / NULL-as-malloc. */ static void realloc_paths(void) { unsigned char *p = malloc(100); unsigned char *q; size_t i; int preserved = 1; check(p != NULL, "realloc setup: malloc(100) non-NULL"); for (i = 0; i < 100; i++) { p[i] = (unsigned char)i; } q = realloc(p, 4096); check(q != NULL, "realloc(p, 4096) grows and returns non-NULL"); for (i = 0; i < 100; i++) { if (q[i] != (unsigned char)i) { preserved = 0; } } check(preserved, "realloc grow preserves all 100 bytes"); q[4095] = 0xee; p = realloc(q, 100); check(p != NULL, "realloc(q, 100) shrinks and returns non-NULL"); preserved = 1; for (i = 0; i < 100; i++) { if (p[i] != (unsigned char)i) { preserved = 0; } } check(preserved, "realloc shrink preserves all 100 bytes"); q = realloc(p, 0); check(q == NULL, "realloc(p, 0) returns NULL"); q = realloc(NULL, 64); check(q != NULL, "realloc(NULL, 64) behaves as malloc"); free(q); } /* 5. aligned_alloc alignment guarantees. */ static void aligned_alloc_paths(void) { void *p = aligned_alloc(64, 4096); void *q = aligned_alloc(256, 8192); check(p != NULL, "aligned_alloc(64, 4096) returns non-NULL"); check(((uintptr_t)p & 63) == 0, "aligned_alloc(64, 4096) is 64-byte aligned"); check(q != NULL, "aligned_alloc(256, 8192) returns non-NULL"); check(((uintptr_t)q & 255) == 0, "aligned_alloc(256, 8192) is 256-byte aligned"); if (p != NULL) { ((unsigned char *)p)[4095] = 0x33; } free(p); free(q); check(__vlibc_malloc_check() == 0, "no live chunks after freeing aligned blocks"); } /* 6. posix_memalign happy + EINVAL negative. */ static void posix_memalign_paths(void) { void *p = (void *)0x1; /* a recognizable sentinel */ void *before = p; int rc = posix_memalign(&p, 16, 100); check(rc == 0, "posix_memalign(&p, 16, 100) returns 0"); check(((uintptr_t)p & 15) == 0, "posix_memalign(16) result is 16-byte aligned"); free(p); p = (void *)0x1; rc = posix_memalign(&p, 3, 8); check(rc == EINVAL, "posix_memalign(&p, 3, 8) returns EINVAL"); check(p == before, "posix_memalign failure leaves *memptr unmodified"); } #if VLIBC_LEVEL_GE(2) /* 7. malloc_usable_size reports at least the request. */ static void usable_size_paths(void) { void *p = malloc(100); if (p != NULL) { check(malloc_usable_size(p) >= 100, "malloc_usable_size(p) >= requested 100"); } else { check(0, "usable-size setup: malloc(100) non-NULL"); } free(p); check(malloc_usable_size(NULL) == 0, "malloc_usable_size(NULL) == 0"); } #endif /* VLIBC_LEVEL_GE(2) */ /* 8. 10k random alloc/free churn ending with zero live chunks. */ static void churn_test(void) { enum { SLOTS = 1024 }; void *slot[SLOTS]; unsigned rng = 0x9e3779b9U; unsigned i; for (i = 0; i < SLOTS; i++) { slot[i] = NULL; } for (i = 0; i < 10000; i++) { unsigned idx = rng_next(&rng) % SLOTS; size_t sz; if (slot[idx] != NULL) { free(slot[idx]); slot[idx] = NULL; continue; } sz = rng_next(&rng) % 4096; if ((rng_next(&rng) & 31) == 0) { sz = 262144; /* occasional 256 KiB mmap block */ } slot[idx] = malloc(sz == 0 ? 1 : sz); check(slot[idx] != NULL, "churn: malloc returns non-NULL"); if (slot[idx] != NULL) { ((unsigned char *)slot[idx])[0] = (unsigned char)sz; } } for (i = 0; i < SLOTS; i++) { free(slot[i]); slot[i] = NULL; } check(__vlibc_malloc_check() == 0, "churn ends with zero live chunks"); } /* Failure scenarios (-f): allocation overflow returns NULL + errno ENOMEM. */ static int failure_scenarios(void) { void *p; /* * SIZE_MAX - 10 overflows the internal chunk-size normalization * deterministically (no syscall, no overcommit dependency — SIZE_MAX/2 * is a legitimate lazy 8 EiB mapping under Linux overcommit and can * legitimately succeed). The noipa proxy keeps GCC from folding the * constant and assuming the alloc_size-undefined call returns non-NULL. */ p = malloc_proxy((size_t)-1 - 10); if (p != NULL) { say(2, "FAIL: malloc(SIZE_MAX-10) returned non-NULL\n"); failures++; } else { say(1, "PASS: malloc(SIZE_MAX-10) -> NULL (overflow)\n"); } p = calloc_proxy(((size_t)-1) / 2, 2); if (p != NULL) { say(2, "FAIL: calloc(SIZE_MAX/2, 2) returned non-NULL\n"); failures++; } else { say(1, "PASS: calloc(SIZE_MAX/2, 2) -> NULL (overflow)\n"); } p = realloc_proxy(NULL, (size_t)-1 - 10); if (p != NULL) { say(2, "FAIL: realloc(NULL, SIZE_MAX-10) returned non-NULL\n"); failures++; } else { say(1, "PASS: realloc(NULL, SIZE_MAX-10) -> NULL (overflow)\n"); } return failures > 0 ? 1 : 0; } int main(int argc, char **argv) { int rc; if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') { /* * The failure scenarios write errno inside the library; under the * host libc that slot is glibc's private TLS state, so leave via * the raw syscall without running host cleanup. */ rc = failure_scenarios(); __syscall1(SYS_exit_group, rc); return rc; /* not reached */ } basic_alloc_free(); zero_size_malloc(); calloc_zeroing(); realloc_paths(); aligned_alloc_paths(); posix_memalign_paths(); #if VLIBC_LEVEL_GE(2) usable_size_paths(); #endif churn_test(); if (failures > 0) { say(2, "FAILED ("); say_dec(2, (unsigned long)failures); say(2, " check(s))\n"); return 1; } say(1, "all malloc tests passed\n"); return 0; }