450 lines
12 KiB
C
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;
|
|
}
|