feat(malloc): first-fit allocator with mmap fallback

This commit is contained in:
2026-09-03 20:44:08 -04:00
parent 06d2ec7c34
commit 5eb3cbe302
3 changed files with 1512 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
#ifndef VLIBC_STDLIB_H
#define VLIBC_STDLIB_H
/*
* vlibc — <stdlib.h>.
*
* This header is the shared home for the stdlib declarations; it currently
* holds only the memory-management family (todo 7). Later todos extend it
* in place: todo 11 adds the numeric conversions (atoi/strtol/strtod/...),
* todo 12 the pseudo-random and search/divide functions
* (rand/srand/qsort/bsearch/abs/div/...), and todo 13 the environment and
* multibyte helpers (getenv/setenv/mblen/mbtowc/...).
*
* Memory management functions, gated by the active compatibility profile
* (see include/vlibc/features.h). Levels are cumulative:
*
* Level 1 (onlyposix): ISO C core + POSIX base — malloc, free, calloc,
* realloc, aligned_alloc, posix_memalign.
* Level 2 (muslmimic): malloc_usable_size (BSD/musl).
*
* This header includes <vlibc/features.h> itself, so the gates below always
* see the configured VLIBC_LEVEL even when the caller included no vlibc
* header first, and <stddef.h> for size_t.
*/
#include <vlibc/features.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Level 1: memory management (always present). */
/*
* Allocate size bytes, 16-byte aligned. The memory is uninitialized.
* malloc(0) returns a unique minimum-size block (never NULL on success).
* NULL + errno ENOMEM on failure.
* malloc: the result does not alias any other pointer and has size bytes.
*/
__attribute__((malloc, alloc_size(1))) void *
malloc(size_t size);
/*
* Release the block at ptr, which must be NULL or a value returned by an
* earlier allocation in this family. free(NULL) is a no-op.
*/
void
free(void *ptr);
/*
* Allocate an array of nmemb elements of size bytes each, all bits zero.
* The product is overflow-checked: on overflow NULL + errno ENOMEM.
* calloc with a zero product returns a unique zeroed minimum-size block.
* malloc: the result does not alias any other pointer and has nmemb*size
* bytes.
*/
__attribute__((malloc, alloc_size(1, 2))) void *
calloc(size_t nmemb, size_t size);
/*
* Resize the block at ptr to size bytes, preserving the first min(old,
* size) bytes. realloc(NULL, size) behaves as malloc(size); realloc(ptr, 0)
* frees ptr and returns NULL. The old block is freed on success and left
* untouched on failure (NULL + errno ENOMEM).
* alloc_size(2): the result has size bytes.
*/
__attribute__((alloc_size(2))) void *
realloc(void *ptr, size_t size);
/*
* Allocate size bytes aligned to alignment. alignment must be a power of
* two that is a multiple of sizeof(void *), and size must be a multiple of
* alignment; a violation fails with NULL + errno EINVAL (a non-power-of-two
* alignment is undefined behavior in C23, so only well-formed arguments
* reach the allocator). size 0 returns NULL. The result is released with
* free.
* malloc + alloc_align(1): the result does not alias any other pointer and
* is aligned to alignment.
*/
__attribute__((malloc, alloc_size(2), alloc_align(1))) void *
aligned_alloc(size_t alignment, size_t size);
/*
* Allocate size bytes at address alignment and store the result in
* *memptr. alignment must be a power of two and a multiple of
* sizeof(void *). Returns 0 on success, EINVAL for a bad alignment, ENOMEM
* on allocation failure. Never sets errno itself, and never modifies
* *memptr on failure. size 0 returns a unique minimum-size block.
*/
__attribute__((access(write_only, 1))) int
posix_memalign(void **memptr, size_t alignment, size_t size);
#if VLIBC_LEVEL_GE(2)
/* Level 2 (muslmimic): BSD/musl extensions. */
/*
* Return the number of bytes actually available in the block at ptr,
* including any internal padding; at least as large as the requested size.
* ptr may be any block returned by the allocator family; NULL returns 0.
* pure: reads memory, no side effects.
*/
__attribute__((pure)) size_t
malloc_usable_size(void *ptr);
#endif /* VLIBC_LEVEL_GE(2) */
#ifdef __cplusplus
}
#endif
#endif /* VLIBC_STDLIB_H */
+951
View File
@@ -0,0 +1,951 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stddef.h>
#include <stdint.h>
#include <errno.h>
#include <vlibc/features.h>
#include "../internal/malloc.h"
#include "../internal/syscall.h"
/*
* vlibc — the heap allocator (todo 7).
*
* A first-fit allocator with segregated size bins, a brk-grown heap, and an
* mmap path for large blocks. Layout reference: musl's single-file malloc;
* this is a clean-room implementation with vlibc's own chunk format.
*
* Chunk layout (every chunk address is 16-aligned; every chunk size is a
* multiple of VLIBC_CHUNK_ALIGN = 32):
*
* struct chunk { size_t psize, csize; } (16-byte header)
* user pointer = chunk + 16
*
* csize low bits (masked with ~3 when reading sizes):
* bit 0 VLIBC_CHUNK_INUSE — chunk is allocated.
* bit 1 VLIBC_CHUNK_ALIGNED — chunk is the raw block of an
* aligned_alloc-style allocation.
* psize low bits (masked with ~7 when reading sizes):
* bit 0 — the previous chunk is in use (1) or free (0).
* bit 2 VLIBC_CHUNK_MMAP — this chunk lives in its own mmap, psize
* holds the mapping length.
*
* Free chunks keep their doubly-linked-list pointers (next/prev) in the
* first 16 bytes of the user area, which is why the minimum chunk is
* 16 (header) + 16 (pointers) = 32 bytes. There is never more than one
* adjacent pair of free chunks: free() coalesces immediately, and the
* top chunk (the free block at the end of the heap) is kept out of the
* bins.
*
* Allocation: requests whose normalized chunk size reaches
* VLIBC_MMAP_THRESHOLD (128 KiB) get a private SYS_mmap mapping (freed
* with SYS_munmap). Everything else searches the bins first-fit (bins 0-3
* hold the exact sizes 32/64/96/128, bins 4-12 hold log-size ranges up to
* just below the mmap threshold), then splits the top chunk, growing the
* heap with SYS_brk as needed. If brk fails, the request falls back to a
* single-chunk mmap.
*
* The brk heap never shrinks below its start address and releases whole
* trailing pages back to the kernel when the top chunk holds more than a
* page beyond the minimum chunk. sbrk() is deliberately not provided (see
* the plan: it conflicts with this allocator).
*
* Aligned allocations (alignment > 16): the raw block is a normal chunk
* marked VLIBC_CHUNK_ALIGNED; the returned pointer sits somewhere inside
* the block with a two-word descriptor directly before it:
*
* [q-16] = alignment (a power of two, so bit 0 is clear)
* [q-8] = raw pointer (the start of the block's user area)
*
* free() tells the two cases apart by the C_INUSE bit of the word at p-16:
* an allocated chunk always has it set, while a descriptor's alignment word
* always has it clear. realloc() on an aligned pointer preserves the
* alignment (via the descriptor) and malloc_usable_size() reports the raw
* block size minus the alignment offset.
*
* malloc(0) returns a unique minimum-size chunk. realloc(p, 0) frees p and
* returns NULL. calloc overflows and aligned_alloc/posix_memalign argument
* violations fail with NULL + errno (ENOMEM/EINVAL); posix_memalign returns
* the error number instead of setting errno and never modifies *memptr on
* failure. Over-requested brk chunks zero themselves when they come from a
* fresh page of the heap, which makes calloc cheap in the common case.
*
* Single-threaded for now: the pthread todo will add locking around the
* global allocator state.
*/
#define VLIBC_CHUNK_OVERHEAD 16 /* psize + csize */
#define VLIBC_CHUNK_ALIGN 32 /* chunk-size granularity */
#define VLIBC_CHUNK_MIN 32 /* smallest chunk: header + 2 pointers */
#define VLIBC_CHUNK_INUSE 1UL /* csize bit 0 */
#define VLIBC_CHUNK_ALIGNED 2UL /* csize bit 1 */
#define VLIBC_CHUNK_MMAP 4UL /* psize bit 2 (private mmap block) */
#define VLIBC_CSIZE_MASK (~(size_t)3)
#define VLIBC_PSIZE_MASK (~(size_t)7)
/* Requests whose normalized chunk size reaches this go straight to mmap. */
#define VLIBC_MMAP_THRESHOLD ((size_t)(128 * 1024))
/* 4 exact-size fast bins (32/64/96/128) + 9 log-range bins. */
#define VLIBC_BIN_COUNT 13
#define VLIBC_BIN_FAST 4
/* Largest user request that cannot overflow the chunk-size normalization. */
#define VLIBC_CHUNK_MAX_USER ((size_t)-1 - VLIBC_CHUNK_ALIGN - VLIBC_CHUNK_OVERHEAD - 1)
/* Minimum heap growth in bytes, rounded up to a page by the extender. */
#define VLIBC_HEAP_GROW ((size_t)32768)
struct vlibc_chunk
{
size_t psize;
size_t csize;
struct vlibc_chunk *next;
struct vlibc_chunk *prev;
};
static struct vlibc_chunk *vlibc_bins[VLIBC_BIN_COUNT];
static struct vlibc_chunk *vlibc_top; /* free block at the end of the heap */
static uintptr_t vlibc_heap_start; /* 0 = heap not yet queried */
static uintptr_t vlibc_heap_end; /* current brk */
static size_t vlibc_live; /* allocated chunks on the heap */
static size_t vlibc_live_mmap; /* allocated mmap chunks */
static void *
vlibc_aligned_core(size_t align, size_t size);
/* Round a user request up to a chunk size (multiple of VLIBC_CHUNK_ALIGN). */
static size_t
vlibc_norm(size_t n)
{
size_t sz =
(n + VLIBC_CHUNK_OVERHEAD + VLIBC_CHUNK_ALIGN - 1) & ~(size_t)(VLIBC_CHUNK_ALIGN - 1);
return sz < VLIBC_CHUNK_MIN ? VLIBC_CHUNK_MIN : sz;
}
/* Index of the bin a free chunk of the given (flag-free) size belongs to. */
static int
vlibc_bin_of(size_t sz)
{
if (sz <= (size_t)(VLIBC_BIN_FAST * VLIBC_CHUNK_ALIGN))
{
return (int)(sz / VLIBC_CHUNK_ALIGN) - 1;
}
return (int)(VLIBC_BIN_FAST + 63 - (unsigned)__builtin_clzll(sz / VLIBC_CHUNK_ALIGN) - 3);
}
static void
vlibc_bin_insert(struct vlibc_chunk *c)
{
int i = vlibc_bin_of(c->csize & VLIBC_CSIZE_MASK);
c->prev = NULL;
c->next = vlibc_bins[i];
if (vlibc_bins[i] != NULL)
{
vlibc_bins[i]->prev = c;
}
vlibc_bins[i] = c;
}
static void
vlibc_bin_remove(struct vlibc_chunk *c)
{
int i = vlibc_bin_of(c->csize & VLIBC_CSIZE_MASK);
if (c->prev != NULL)
{
c->prev->next = c->next;
}
else
{
vlibc_bins[i] = c->next;
}
if (c->next != NULL)
{
c->next->prev = c->prev;
}
c->next = NULL;
c->prev = NULL;
}
/* Record c (size sz, flag-free) as allocated and update the next header. */
static void
vlibc_mark_alloc(struct vlibc_chunk *c, size_t sz)
{
struct vlibc_chunk *next = (struct vlibc_chunk *)((char *)c + sz);
c->csize = sz | VLIBC_CHUNK_INUSE;
if ((uintptr_t)next < vlibc_heap_end && next != vlibc_top)
{
next->psize = c->csize;
}
}
/*
* Query the brk once and remember the heap window. Returns 0 on success;
* -1 leaves the window uninitialized so callers fall back to mmap.
*/
static int
vlibc_heap_init(void)
{
long r;
if (vlibc_heap_start != 0)
{
return 0;
}
r = __syscall1(SYS_brk, 0);
if (r < 0)
{
return -1;
}
vlibc_heap_start = ((uintptr_t)r + 15) & ~(uintptr_t)15;
vlibc_heap_end = vlibc_heap_start;
return 0;
}
/*
* Grow the heap by at least need bytes. On success the top chunk covers the
* new space (created if the heap was fully consumed); on failure the state
* is unchanged and the caller falls back to a private mmap.
*/
static int
vlibc_heap_extend(size_t need)
{
size_t want = need > VLIBC_HEAP_GROW ? need : VLIBC_HEAP_GROW;
uintptr_t new_end;
long r;
want = (want + 4095) & ~(size_t)4095;
new_end = vlibc_heap_end + want;
r = __syscall1(SYS_brk, (long)new_end);
if ((uintptr_t)r != new_end)
{
return -1;
}
if (vlibc_top != NULL)
{
vlibc_top->csize += want;
}
else
{
vlibc_top = (struct vlibc_chunk *)vlibc_heap_end;
vlibc_top->psize = 0 | VLIBC_CHUNK_INUSE; /* prev size unknown */
vlibc_top->csize = want;
}
vlibc_heap_end = new_end;
return 0;
}
/*
* Give whole trailing pages of the top chunk back to the kernel. Always
* keeps at least a minimum chunk so the heap window never closes.
*/
static void
vlibc_heap_trim(void)
{
size_t sz = vlibc_top->csize;
size_t release = (sz - VLIBC_CHUNK_MIN) & ~(size_t)4095;
uintptr_t new_end;
long r;
if (release < 4096)
{
return;
}
new_end = (uintptr_t)vlibc_top + sz - release;
r = __syscall1(SYS_brk, (long)new_end);
if ((uintptr_t)r == new_end)
{
vlibc_top->csize = sz - release;
vlibc_heap_end = new_end;
}
}
/* Private mmap block of normalized size nn. NULL on failure. */
static void *
vlibc_mmap_chunk(size_t nn)
{
size_t len;
long r;
struct vlibc_chunk *c;
if (nn > (size_t)-1 - VLIBC_CHUNK_OVERHEAD - 4095)
{
return NULL; /* the rounded mapping length would overflow */
}
len = (nn + VLIBC_CHUNK_OVERHEAD + 4095) & ~(size_t)4095;
r = __syscall6(SYS_mmap, 0, (long)len, 3, 0x22, -1, 0);
if ((uintptr_t)r > (uintptr_t)-4096)
{
return NULL; /* kernel returned -errno */
}
c = (struct vlibc_chunk *)r;
c->psize = len | VLIBC_CHUNK_MMAP;
c->csize = nn | VLIBC_CHUNK_INUSE;
vlibc_live_mmap++;
return (char *)c + VLIBC_CHUNK_OVERHEAD;
}
/* First free chunk with size >= nn anywhere in bins [bin_of(nn), end). */
static struct vlibc_chunk *
vlibc_bin_find(size_t nn)
{
int i;
for (i = vlibc_bin_of(nn); i < VLIBC_BIN_COUNT; i++)
{
struct vlibc_chunk *c;
for (c = vlibc_bins[i]; c != NULL; c = c->next)
{
if ((c->csize & VLIBC_CSIZE_MASK) >= nn)
{
return c;
}
}
}
return NULL;
}
/*
* Split free chunk c (current size s) into an nn-byte allocation and a free
* remainder. Requires s >= nn + MIN. The chunk after the original extent
* gets its psize pointed at the remainder: without that update a later free
* there would walk back over the stale (too large) size and land on the
* wrong chunk. When the split chunk reaches the top chunk, the remainder is
* absorbed into the top instead of entering a bin — a free chunk directly
* before top would violate the coalescing invariant and leave the top's
* predecessor size stale.
*/
static void
vlibc_split(struct vlibc_chunk *c, size_t s, size_t nn)
{
struct vlibc_chunk *rem = (struct vlibc_chunk *)((char *)c + nn);
struct vlibc_chunk *after = (struct vlibc_chunk *)((char *)c + s);
rem->psize = nn; /* prev still free at this instant */
rem->csize = s - nn;
c->csize = nn;
if (after == vlibc_top)
{
rem->csize += vlibc_top->csize;
rem->psize = nn | VLIBC_CHUNK_INUSE; /* c becomes allocated below */
vlibc_top = rem;
}
else if (after == (struct vlibc_chunk *)vlibc_heap_end)
{
rem->psize = nn | VLIBC_CHUNK_INUSE;
vlibc_top = rem;
}
else
{
if ((uintptr_t)after < vlibc_heap_end)
{
after->psize = s - nn;
}
vlibc_bin_insert(rem);
}
}
/*
* Carve nn bytes out of the top chunk and return the user pointer. When the
* remainder is at least a minimum chunk it becomes the new top; otherwise
* the whole top is consumed and the next extension starts a fresh one.
*/
static void *
vlibc_carve_top(size_t nn)
{
struct vlibc_chunk *c = vlibc_top;
size_t s = c->csize;
if (s >= nn + VLIBC_CHUNK_MIN)
{
struct vlibc_chunk *rem = (struct vlibc_chunk *)((char *)c + nn);
rem->psize = nn | VLIBC_CHUNK_INUSE;
rem->csize = s - nn;
c->csize = nn | VLIBC_CHUNK_INUSE;
vlibc_top = rem;
}
else
{
c->csize = s | VLIBC_CHUNK_INUSE;
vlibc_top = NULL;
}
vlibc_live++;
return (char *)c + VLIBC_CHUNK_OVERHEAD;
}
/*
* The allocator core (the internal seam src/internal/malloc.h documents).
* Allocates n bytes, 16-byte aligned; NULL + errno ENOMEM on failure. May
* return a unique pointer even when n == 0.
*/
hidden void *
__libc_malloc(size_t n) // NOLINT(bugprone-reserved-identifier)
{
size_t nn;
struct vlibc_chunk *c;
if (n > VLIBC_CHUNK_MAX_USER)
{
errno = ENOMEM;
return NULL;
}
nn = vlibc_norm(n);
if (nn >= VLIBC_MMAP_THRESHOLD)
{
void *p = vlibc_mmap_chunk(nn);
if (p == NULL)
{
errno = ENOMEM;
}
return p;
}
c = vlibc_bin_find(nn);
if (c != NULL)
{
size_t s = c->csize & VLIBC_CSIZE_MASK;
vlibc_bin_remove(c);
if (s >= nn + VLIBC_CHUNK_MIN)
{
vlibc_split(c, s, nn);
vlibc_mark_alloc(c, nn);
}
else
{
vlibc_mark_alloc(c, s);
}
vlibc_live++;
return (char *)c + VLIBC_CHUNK_OVERHEAD;
}
if (vlibc_heap_init() == 0 && vlibc_heap_extend(nn) == 0)
{
return vlibc_carve_top(nn);
}
{
void *p = vlibc_mmap_chunk(nn);
if (p == NULL)
{
errno = ENOMEM;
}
return p;
}
}
/*
* Release a block returned by __libc_malloc; NULL is a no-op. Coalesces
* with adjacent free chunks and the top chunk, then trims trailing pages.
*/
hidden void
__libc_free(void *p) // NOLINT(bugprone-reserved-identifier)
{
struct vlibc_chunk *c;
size_t sz;
if (p == NULL)
{
return;
}
c = (struct vlibc_chunk *)((char *)p - VLIBC_CHUNK_OVERHEAD);
if ((c->csize & VLIBC_CHUNK_INUSE) == 0)
{
/* Aligned allocation: the descriptor sits right before p. */
void *raw = *(void **)((char *)p - sizeof(void *));
p = raw;
c = (struct vlibc_chunk *)((char *)raw - VLIBC_CHUNK_OVERHEAD);
}
if ((c->psize & VLIBC_CHUNK_MMAP) != 0)
{
(void)__syscall2(SYS_munmap, (long)c, (long)(c->psize & VLIBC_PSIZE_MASK));
vlibc_live_mmap--;
return;
}
vlibc_live--;
sz = c->csize & VLIBC_CSIZE_MASK;
c->csize = sz; /* clear INUSE and ALIGNED */
/* Coalesce with the following chunk, top chunk first. */
{
struct vlibc_chunk *next = (struct vlibc_chunk *)((char *)c + sz);
if (next == vlibc_top)
{
c->csize += vlibc_top->csize;
vlibc_top = c;
}
else if ((uintptr_t)next < vlibc_heap_end && (next->csize & VLIBC_CHUNK_INUSE) == 0)
{
struct vlibc_chunk *after;
vlibc_bin_remove(next);
c->csize += next->csize;
after = (struct vlibc_chunk *)((char *)c + c->csize);
if ((uintptr_t)after < vlibc_heap_end)
{
after->psize = c->csize;
}
}
}
/* Coalesce with the preceding chunk when it is free. */
if ((c->psize & 1) == 0)
{
struct vlibc_chunk *prev =
(struct vlibc_chunk *)((char *)c - (c->psize & VLIBC_PSIZE_MASK));
struct vlibc_chunk *after;
vlibc_bin_remove(prev);
prev->csize += c->csize;
if (c == vlibc_top)
{
vlibc_top = prev;
}
c = prev;
after = (struct vlibc_chunk *)((char *)c + c->csize);
if ((uintptr_t)after < vlibc_heap_end)
{
after->psize = c->csize;
}
}
/*
* A free block that ends exactly at the heap end is — or joins — the
* top chunk. This must also run when the top was fully consumed
* (vlibc_top == NULL): a freed trailing chunk that merely entered a bin
* would leave the allocator without a top, and a later extension would
* then grow over the bin's space while stale top state lets trim shrink
* the brk across live chunks.
*/
{
struct vlibc_chunk *end = (struct vlibc_chunk *)((char *)c + (c->csize & VLIBC_CSIZE_MASK));
if (end == vlibc_top)
{
c->csize += vlibc_top->csize;
vlibc_top = c;
}
else if (end == (struct vlibc_chunk *)vlibc_heap_end)
{
vlibc_top = c;
}
}
if (c == vlibc_top)
{
vlibc_heap_trim();
}
else
{
vlibc_bin_insert(c);
}
}
/*
* The public malloc: thin forward to the internal seam so that every
* allocation path in the library shares one implementation.
*/
void *
malloc(size_t n)
{
return __libc_malloc(n);
}
/* The public free: thin forward to the internal seam. */
void
free(void *p)
{
__libc_free(p);
}
/*
* Allocate zeroed memory for nmemb elements of size bytes. The product is
* overflow-checked; NULL + errno ENOMEM on failure. calloc(n, 0) and
* calloc(0, n) return a unique zeroed minimum-size chunk like malloc(0).
*/
void *
calloc(size_t nmemb, size_t size)
{
size_t total;
void *p;
struct vlibc_chunk *c;
size_t usable;
size_t i;
if (size != 0 && nmemb > (size_t)-1 / size)
{
errno = ENOMEM;
return NULL;
}
total = nmemb * size;
p = malloc(total);
if (p == NULL)
{
return NULL;
}
c = (struct vlibc_chunk *)((char *)p - VLIBC_CHUNK_OVERHEAD);
if ((c->psize & VLIBC_CHUNK_MMAP) != 0)
{
return p; /* a fresh mmap is already zero */
}
usable = (c->csize & VLIBC_CSIZE_MASK) - VLIBC_CHUNK_OVERHEAD;
for (i = 0; i < usable; i++)
{
((unsigned char *)p)[i] = 0;
}
return p;
}
/*
* Resize the block at p to n bytes. realloc(NULL, n) is malloc(n);
* realloc(p, 0) frees p and returns NULL. Growth first tries to merge the
* following free chunk (or the top chunk); otherwise the block is copied to
* a fresh allocation and freed. On failure the original block is untouched
* and errno is ENOMEM.
*/
void *
realloc(void *p, size_t n)
{
struct vlibc_chunk *c;
size_t sz;
size_t nn;
void *np;
size_t old;
size_t copy;
size_t i;
if (p == NULL)
{
return malloc(n);
}
if (n == 0)
{
free(p);
return NULL;
}
if (n > VLIBC_CHUNK_MAX_USER)
{
errno = ENOMEM;
return NULL;
}
nn = vlibc_norm(n);
c = (struct vlibc_chunk *)((char *)p - VLIBC_CHUNK_OVERHEAD);
if ((c->csize & VLIBC_CHUNK_INUSE) == 0)
{
/* Aligned allocation: re-allocate with the original alignment. */
size_t align = *(size_t *)((char *)p - 2 * sizeof(size_t));
void *raw = *(void **)((char *)p - sizeof(void *));
struct vlibc_chunk *rc = (struct vlibc_chunk *)((char *)raw - VLIBC_CHUNK_OVERHEAD);
old =
(rc->csize & VLIBC_CSIZE_MASK) - VLIBC_CHUNK_OVERHEAD - ((uintptr_t)p - (uintptr_t)raw);
if (n % align != 0)
{
errno = EINVAL;
return NULL;
}
np = vlibc_aligned_core(align, n);
if (np == NULL)
{
return NULL;
}
copy = old < n ? old : n;
for (i = 0; i < copy; i++)
{
((unsigned char *)np)[i] = ((unsigned char *)p)[i];
}
free(p);
return np;
}
if ((c->psize & VLIBC_CHUNK_MMAP) != 0)
{
old = (c->csize & VLIBC_CSIZE_MASK) - VLIBC_CHUNK_OVERHEAD;
np = malloc(n);
if (np == NULL)
{
return NULL;
}
copy = old < n ? old : n;
for (i = 0; i < copy; i++)
{
((unsigned char *)np)[i] = ((unsigned char *)p)[i];
}
free(p);
return np;
}
sz = c->csize & VLIBC_CSIZE_MASK;
if (nn <= sz)
{
if (sz >= nn + VLIBC_CHUNK_MIN)
{
/* Shrink in place, releasing the tail as a free chunk. */
vlibc_split(c, sz, nn);
vlibc_mark_alloc(c, nn);
}
return p;
}
/* Try to grow into the following free chunk (or the top chunk). */
{
struct vlibc_chunk *next = (struct vlibc_chunk *)((char *)c + sz);
if (next == vlibc_top)
{
size_t total = sz + vlibc_top->csize;
if (total >= nn)
{
if (total >= nn + VLIBC_CHUNK_MIN)
{
struct vlibc_chunk *rem = (struct vlibc_chunk *)((char *)c + nn);
rem->psize = nn | VLIBC_CHUNK_INUSE;
rem->csize = total - nn;
c->csize = nn | VLIBC_CHUNK_INUSE;
vlibc_top = rem;
}
else
{
c->csize = total | VLIBC_CHUNK_INUSE;
vlibc_top = NULL;
}
return p;
}
}
else if ((uintptr_t)next < vlibc_heap_end && (next->csize & VLIBC_CHUNK_INUSE) == 0)
{
size_t total = sz + (next->csize & VLIBC_CSIZE_MASK);
if (total >= nn)
{
vlibc_bin_remove(next);
if (total >= nn + VLIBC_CHUNK_MIN)
{
struct vlibc_chunk *rem = (struct vlibc_chunk *)((char *)c + nn);
struct vlibc_chunk *after;
rem->psize = nn | VLIBC_CHUNK_INUSE;
rem->csize = total - nn;
c->csize = nn | VLIBC_CHUNK_INUSE;
after = (struct vlibc_chunk *)((char *)rem + rem->csize);
if (after == vlibc_top)
{
rem->csize += vlibc_top->csize;
vlibc_top = rem;
}
else if (after == (struct vlibc_chunk *)vlibc_heap_end)
{
vlibc_top = rem;
}
else
{
vlibc_bin_insert(rem);
if ((uintptr_t)after < vlibc_heap_end)
{
after->psize = rem->csize;
}
}
}
else
{
struct vlibc_chunk *after = (struct vlibc_chunk *)((char *)c + total);
c->csize = total | VLIBC_CHUNK_INUSE;
if ((uintptr_t)after < vlibc_heap_end)
{
after->psize = c->csize;
}
}
return p;
}
/* Keep next linked; fall through to alloc-copy-free. */
}
}
/* Allocate fresh, copy the smaller of the two payloads, free the old. */
old = sz - VLIBC_CHUNK_OVERHEAD;
np = malloc(n);
if (np == NULL)
{
return NULL;
}
copy = old < n ? old : n;
for (i = 0; i < copy; i++)
{
((unsigned char *)np)[i] = ((unsigned char *)p)[i];
}
free(p);
return np;
}
/*
* Core of the aligned family: allocate size bytes aligned to align (a
* power of two greater than 16). The returned pointer has a two-word
* descriptor directly before it ([q-16] = align, [q-8] = raw pointer).
*/
static void *
vlibc_aligned_core(size_t align, size_t size)
{
void *raw;
void *q;
struct vlibc_chunk *c;
raw = malloc(size + align + VLIBC_CHUNK_ALIGN);
if (raw == NULL)
{
return NULL;
}
q = (void *)(((uintptr_t)raw + VLIBC_CHUNK_ALIGN + align - 1) & -(uintptr_t)align);
*(size_t *)((char *)q - 2 * sizeof(size_t)) = align;
*(void **)((char *)q - sizeof(void *)) = raw;
c = (struct vlibc_chunk *)((char *)raw - VLIBC_CHUNK_OVERHEAD);
c->csize |= VLIBC_CHUNK_ALIGNED;
return q;
}
/*
* Allocate size bytes aligned to alignment. alignment must be a power of
* two that is a multiple of sizeof(void *), and size must be a multiple of
* alignment; a violation fails with NULL + errno EINVAL (a non-power-of-two
* alignment is undefined behavior in C23, so only well-formed arguments
* reach the allocator). size 0 returns NULL. The returned pointer is a
* valid malloc block and is released with free.
*/
void *
aligned_alloc(size_t alignment, size_t size)
{
if ((alignment & (alignment - 1)) != 0 || alignment % sizeof(void *) != 0 ||
size % alignment != 0)
{
errno = EINVAL;
return NULL;
}
if (size == 0)
{
return NULL;
}
if (alignment <= VLIBC_CHUNK_OVERHEAD)
{
return malloc(size);
}
return vlibc_aligned_core(alignment, size);
}
/*
* Allocate size bytes at address alignment and store the result in
* *memptr. alignment must be a power of two and a multiple of
* sizeof(void *). Returns 0 on success, EINVAL for a bad alignment, ENOMEM
* on allocation failure; never sets errno itself and never modifies
* *memptr on failure. size 0 returns a unique minimum-size block.
*/
int
posix_memalign(void **memptr, size_t alignment, size_t size)
{
void *p;
if (memptr == NULL || (alignment & (alignment - 1)) != 0 || alignment % sizeof(void *) != 0)
{
return EINVAL;
}
p = vlibc_aligned_core(alignment, size == 0 ? 1 : size);
if (p == NULL)
{
return ENOMEM;
}
*memptr = p;
return 0;
}
#if VLIBC_LEVEL_GE(2)
/*
* Return the number of bytes actually available in the block at p,
* including any internal padding. p may be any block returned by the
* allocator family. NULL returns 0.
*/
size_t
malloc_usable_size(void *p)
{
struct vlibc_chunk *c;
if (p == NULL)
{
return 0;
}
c = (struct vlibc_chunk *)((char *)p - VLIBC_CHUNK_OVERHEAD);
if ((c->csize & VLIBC_CHUNK_INUSE) == 0)
{
/* Aligned allocation: report the raw block minus the offset. */
void *raw = *(void **)((char *)p - sizeof(void *));
struct vlibc_chunk *rc = (struct vlibc_chunk *)((char *)raw - VLIBC_CHUNK_OVERHEAD);
if ((rc->psize & VLIBC_CHUNK_MMAP) != 0)
{
return (rc->psize & VLIBC_PSIZE_MASK) - VLIBC_CHUNK_OVERHEAD -
((uintptr_t)p - (uintptr_t)raw);
}
return (rc->csize & VLIBC_CSIZE_MASK) - VLIBC_CHUNK_OVERHEAD -
((uintptr_t)p - (uintptr_t)raw);
}
if ((c->psize & VLIBC_CHUNK_MMAP) != 0)
{
return (c->psize & VLIBC_PSIZE_MASK) - VLIBC_CHUNK_OVERHEAD;
}
return (c->csize & VLIBC_CSIZE_MASK) - VLIBC_CHUNK_OVERHEAD;
}
#endif /* VLIBC_LEVEL_GE(2) */
/*
* Internal consistency hook used by the test suite: walk the whole heap
* chunk-by-chunk (an independent account of every live allocation), sum
* the in-use chunks, and compare against the allocator's own counters.
* Returns the total number of live blocks, or (size_t)-1 when the heap
* walk disagrees with the counters (corruption or an accounting bug).
*/
hidden size_t
__vlibc_malloc_check(void) // NOLINT(bugprone-reserved-identifier)
{
uintptr_t cur;
size_t live = 0;
if (vlibc_heap_start == 0)
{
return vlibc_live + vlibc_live_mmap == 0 ? 0 : (size_t)-1;
}
cur = vlibc_heap_start;
while (cur + VLIBC_CHUNK_MIN <= vlibc_heap_end)
{
struct vlibc_chunk *c = (struct vlibc_chunk *)cur;
size_t sz = c->csize & VLIBC_CSIZE_MASK;
if (sz < VLIBC_CHUNK_MIN || sz % VLIBC_CHUNK_ALIGN != 0 || cur + sz > vlibc_heap_end)
{
return (size_t)-1;
}
if ((c->csize & VLIBC_CHUNK_INUSE) != 0)
{
live++;
}
cur += sz;
}
if (cur != vlibc_heap_end || live != vlibc_live)
{
return (size_t)-1;
}
return live + vlibc_live_mmap;
}
+449
View File
@@ -0,0 +1,449 @@
/*
* 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;
}