Files

450 lines
12 KiB
C

/*
* 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 <stdio.h>
* 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 <stddef.h>
#include <stdint.h>
#include <errno.h>
#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;
}