AxlMem — Memory Allocation

AxlMem — Memory Allocation

Memory allocation with dmalloc-inspired debug features. Size-tracking headers enable axl_realloc without passing the old size. Debug builds add fence-post guards, alloc/free fill patterns, file/line tracking, and leak reporting.

Header: <axl/axl-mem.h>

Overview

AXL provides its own allocator on top of UEFI’s pool memory. All allocated memory is tracked with a small header that stores the block size, enabling axl_realloc and debug features without requiring the caller to pass the old size.

Do not mix axl_malloc/axl_free with UEFI’s AllocatePool/FreePool — they use different headers.

Debug vs. Release

In debug builds (-DAXL_MEM_DEBUG, the default for make):

  • Fill patterns: newly allocated memory is filled with 0xDA; freed memory is filled with 0xDD. Use-after-free often manifests as reads of 0xDD.

  • Fence-post guards: 8 bytes of 0xFD are placed before and after each allocation. axl_mem_check(ptr) verifies these guards.

  • File/line tracking: each allocation records __FILE__ and __LINE__ for leak reports.

  • Leak reporting: axl_mem_dump_leaks() prints all outstanding allocations with their sizes and source locations.

  • OOM fault injection: axl_mem_fail_next_alloc(N) arms the Nth subsequent allocation to return NULL without touching the backend. Used by unit tests to exercise caller-side error paths (rollback, cleanup, error logging) that would otherwise be unreachable.

In release builds (make BUILD=RELEASE), these features are disabled and the allocator has minimal overhead.

OOM Fault Injection

AXL’s error-handling paths — allocator OOM, hash-table insert rollback, radix-tree split fallbacks, HTTP cache eviction, etc. — are only ever taken under memory pressure, which is hard to trigger naturally in tests. axl_mem_fail_next_alloc lets a test deterministically force a specific allocation to fail:

// Exercise the OOM path of a constructor
axl_mem_fail_next_alloc(1);
AxlHashTable *h = axl_hash_table_new_str();
assert(h == NULL);

// Let the first N-1 allocations through, fail the Nth
axl_mem_fail_next_alloc(3);
void *a = axl_malloc(16);  // succeeds
void *b = axl_malloc(16);  // succeeds
void *c = axl_malloc(16);  // returns NULL

// Passing 0 disables injection (the default state)
axl_mem_fail_next_alloc(0);

After the failure fires, the counter clears itself and subsequent allocations succeed normally. Because every allocation path (axl_calloc, axl_realloc, axl_strdup, axl_memdup) routes through the same axl_malloc_impl, one hook point catches them all. The injection logs at axl_debug level so it doesn’t pollute the real-OOM error signal in test output.

The hook is active in both DEBUG and RELEASE — the counter check is one well-predicted branch on the malloc path, cheap enough that production builds keep the contract too. The fence/leak-tracking machinery (alloc-fill, fences, alloc-list) stays DEBUG-only; only the fail-next-alloc counter is universal.

RAII Auto-Cleanup

AXL provides GLib-style auto-cleanup macros using __attribute__((cleanup)):

// Automatically freed when 's' goes out of scope
AXL_AUTO_FREE char *s = axl_strdup("hello");

// Automatically freed when 'h' goes out of scope
AXL_AUTOPTR(AxlHashTable) h = axl_hash_table_new_str();

if (error_condition) {
    return -1;  // both 's' and 'h' are freed automatically
}

Important: Always initialize at declaration. Never use with goto that jumps over the declaration.

Quick Reference

#include <axl.h>

// Basic allocation
char *buf = axl_malloc(256);
void *copy = axl_memdup(original, size);
char *str = axl_strdup("hello");

// Typed allocation (zero-initialized)
MyStruct *s = axl_new(MyStruct);
int *arr = axl_new_array(int, 100);

// Free (NULL-safe)
axl_free(buf);

// Debug: check for leaks before exit
axl_mem_dump_leaks();

See also: AxlString for auto-growing string builders, AxlStream / AxlFs for I/O.

API Reference

Defines

AXL_AUTO_FREE
AXL_ARRAY_SIZE(a)

Get the number of elements in a static array.

Only works on actual arrays, not pointers. Produces a compile-time constant suitable for use in static initializers.

AXL_SIGNATURE_32(a, b, c, d)

Construct a 32-bit signature from four ASCII characters.

Encodes characters in little-endian order. Commonly used for structure validation signatures in UEFI drivers.

AXL_CONTAINER_OF(ptr, type, member)

Derive a pointer to the enclosing structure from a member pointer.

Given a pointer to a struct member, returns a pointer to the containing structure. Equivalent to Linux kernel’s container_of and EDK2’s CR/BASE_CR macros.

Parameters:
  • ptr – pointer to the member

  • type – type of the containing structure

  • member – name of the member within the structure

Functions

void *axl_malloc(size_t size)

Allocate size bytes of uninitialized memory.

axl-mem.h:

Memory allocation with dmalloc-inspired debug features.

Size-tracking header enables realloc without old_size. DEBUG builds add fence-post guards, alloc/free fill patterns, file/line tracking, and leak reporting.

Do NOT free axl_malloc’d memory with FreePool (it has a header).

Parameters:
  • size – number of bytes to allocate

Returns:

pointer to allocated memory, or NULL on failure. Free with axl_free().

void *axl_calloc(size_t count, size_t size)

Allocate zero-initialized memory for count elements of size bytes each.

Parameters:
  • count – number of elements

  • size – size of each element in bytes

Returns:

pointer to allocated memory, or NULL on failure. Free with axl_free().

void *axl_realloc(void *ptr, size_t size)

Resize a previously allocated block to size bytes.

Contents are preserved up to the smaller of old and new sizes. If ptr is NULL, behaves like axl_malloc().

Parameters:
  • ptr – pointer from axl_malloc/axl_calloc/axl_realloc, or NULL

  • size – new size in bytes

Returns:

pointer to reallocated memory, or NULL on failure (original block is unchanged).

void axl_free(void *ptr)

Free memory allocated by axl_malloc, axl_calloc, axl_realloc, axl_strdup, or axl_memdup. NULL-safe.

Parameters:
  • ptr – pointer to free, or NULL

char *axl_strdup(const char *s)

Duplicate a NUL-terminated string.

Parameters:
  • s – string to duplicate, or NULL

Returns:

newly allocated copy, or NULL on failure. Free with axl_free().

void *axl_memdup(const void *src, size_t size)

Duplicate size bytes of memory from src.

Parameters:
  • src – source buffer

  • size – number of bytes to copy

Returns:

newly allocated copy, or NULL on failure. Free with axl_free().

void *axl_new(size_t Type)

Allocate and zero-initialize a single instance of a type.

Usage: MyStruct *p = axl_new(MyStruct);

Parameters:
  • Type – the type to allocate (passed as a type name)

Returns:

typed pointer, or NULL on failure. Free with axl_free().

void *axl_new_array(size_t Type, size_t Count)

Allocate and zero-initialize an array of elements.

Usage: int *arr = axl_new_array(int, 100);

Parameters:
  • Type – the type to allocate (passed as a type name)

  • Count – number of elements

Returns:

typed pointer, or NULL on failure. Free with axl_free().

void axl_free_impl(void *ptr)

Auto-free a heap pointer when it goes out of scope.

Works with axl_malloc, axl_strdup, axl_memdup, and any pointer freed by axl_free().

AXL_AUTO_FREE char *s = axl_strdup("hello");
if (error) return -1;  // s is freed automatically

IMPORTANT: Always initialize at declaration. Never use with goto that jumps over the declaration. Cleanup runs at scope exit regardless of whether the variable was assigned.

static inline void _axl_auto_free_func(void *p)
int axl_alloc_pages(size_t count, uint64_t *phys_addr)

Allocate contiguous page-aligned memory.

Allocates count pages (each 4096 bytes) of contiguous physical memory. Suitable for DMA buffers, RAM disks, and other uses requiring physical address alignment.

Parameters:
  • count – number of 4KB pages to allocate

  • phys_addr – [out] receives physical address

Returns:

AXL_OK on success, AXL_ERR on error.

void axl_free_pages(uint64_t phys_addr, size_t count)

Free page-aligned memory allocated by axl_alloc_pages.

Parameters:
  • phys_addr – physical address from axl_alloc_pages

  • count – number of pages (must match alloc)

void axl_mem_get_stats(AxlMemStats *stats)

Get current allocation statistics.

Parameters:
  • stats – [out] receives statistics

void axl_mem_dump_leaks(void)

Print all outstanding allocations to the log (debug builds).

Each leaked block is reported with its size, file, and line number. No-op in release builds.

bool axl_mem_check(const void *ptr)

Validate a heap pointer’s fence-post guards (debug builds).

Parameters:
  • ptr – pointer to validate

Returns:

true if valid, false if corrupted or not an axl_malloc’d pointer. Always returns true in release builds.

void axl_mem_fail_next_alloc(size_t n)

Inject an out-of-memory failure for testing error paths.

After calling axl_mem_fail_next_alloc(N) with N > 0, the Nth subsequent allocation through the AXL allocator (axl_malloc/axl_calloc/axl_realloc/axl_strdup/axl_memdup) returns NULL without touching the backend. Allocations before the Nth succeed normally. After the failure fires, the counter resets to 0 (disabled) so subsequent allocations also succeed.

// Exercise the OOM path of some constructor.
axl_mem_fail_next_alloc(1);
MyThing *t = my_thing_new();
assert(t == NULL);

Pass 0 to disable injection. Available in both DEBUG and RELEASE builds — the cost on the malloc path is one well-predicted branch. The fence/leak-tracking machinery stays DEBUG-only; only the fail-counter is universal.

Parameters:
  • n – fail the Nth next alloc (1 = next, 0 = disabled)

struct AxlMemStats

Public Members

size_t count
size_t bytes
size_t total_count
size_t total_bytes