Memory allocation
SML Memory Allocators
The SML Allocators library provides a set of custom wrappers around standard C memory management functions (malloc
, calloc
, realloc
, free
). These wrappers are designed to enhance safety by performing critical checks for integer overflows and handling zero-size allocations consistently. They also include features for testing and debugging, such as simulating allocation failures.
Table of Contents
Core Concepts
Safety First: Overflow Checks
A common vulnerability in C memory allocation is integer overflow. If you try to allocate how_much * sizeof_mem
bytes, the multiplication itself can wrap around to a small number, causing a much smaller buffer to be allocated than expected. Subsequent writes can then lead to a buffer overflow. The SML allocators prevent this by checking for potential overflows before performing the multiplication.
// Overflow check inside sml_alloc
if (how_much > 0 && sizeof_mem > 0 && (ULLONG_MAX / how_much < sizeof_mem)) {
fprintf(stderr, "Overflow detected...");
return NULL;
}
Consistent Zero-Size Handling
The behavior of malloc(0)
is implementation-defined; it can return NULL
or a valid pointer that cannot be dereferenced. To ensure predictable behavior, all SML allocation functions that are asked to allocate zero bytes will consistently return NULL
.
Debugging and Failure Simulation
The library can be compiled with special flags to aid in debugging and testing:
DEBUG
: When defined,sml_alloc
will internally usesml_calloc
to ensure all allocated memory is zero-initialized. This helps catch bugs related to uninitialized memory.SML_MALLOC_FAILURE
/SML_CALLOC_FAILURE
: When defined, these flags causesml_malloc
orsml_calloc
to always returnNULL
, allowing you to test your application’s error-handling logic for allocation failures.
You can enable these features by passing flags to your compiler. For example:
gcc -DDEBUG my_app.c -o my_app
Core Allocator Functions
These functions are the basic building blocks for memory allocation.
sml_malloc
A safer wrapper around malloc
.
void *sml_malloc(unsigned long long size);
- Description: Allocates
size
bytes of uninitialized memory. - Safety: Returns
NULL
ifsize
is 0. - Return Value: A pointer to the allocated memory, or
NULL
on failure.
Example:
// Allocate space for a single integer
int *my_int = sml_malloc(sizeof(int));
if (my_int == NULL) {
// Handle allocation failure
}
*my_int = 42;
free(my_int);
sml_calloc
A safer wrapper around calloc
.
void *sml_calloc(unsigned long long how_much, unsigned long long sizeof_mem);
- Description: Allocates memory for an array of
how_much
elements, each ofsizeof_mem
size. The allocated memory is initialized to zero. - Safety: Includes an integer overflow check on
how_much * sizeof_mem
. ReturnsNULL
if either argument is 0. - Return Value: A pointer to the allocated memory, or
NULL
on failure.
Example:
// Allocate an array of 10 integers, all initialized to 0
int *my_array = sml_calloc(10, sizeof(int));
if (my_array == NULL) {
// Handle allocation failure
}
// my_array[0] is guaranteed to be 0
free(my_array);
sml_alloc
A general-purpose, debug-friendly allocator.
void *sml_alloc(unsigned long long how_much, unsigned long long sizeof_mem);
- Description: A higher-level allocation function that delegates to
sml_malloc
orsml_calloc
depending on the build mode. - Behavior:
- In a release build (no
DEBUG
flag), it acts likesml_malloc(how_much * sizeof_mem)
for performance. - In a debug build (
-DDEBUG
), it acts likesml_calloc(how_much, sizeof_mem)
to provide zero-initialized memory, which helps in finding bugs. - Safety: Includes the same overflow check as
sml_calloc
.
Reallocation Functions
These functions are used to resize previously allocated memory blocks.
sml_realloc
A safer wrapper around realloc
with an optional retry mechanism.
void *sml_realloc(void *ptr, unsigned long long new_requested_size, short unsigned retry);
- Description: Changes the size of the memory block pointed to by
ptr
tonew_requested_size
. The contents will be unchanged up to the minimum of the old and new sizes. - Standard Behavior:
- If
ptr
isNULL
, it behaves likesml_malloc(new_requested_size)
. - If
new_requested_size
is 0, it behaves likefree(ptr)
and returnsNULL
. - Retry Mechanism: The
retry
parameter specifies how many additional times to attempt reallocation if it fails. This can be useful in systems where memory fragmentation might cause temporary failures. A value of0
means it will try only once. - Return Value: A pointer to the reallocated memory, or
NULL
on failure. If it fails, the originalptr
remains valid.
Example:
char *buffer = sml_malloc(10);
strcpy(buffer, "hello");
// Grow the buffer to 20 bytes, with 2 retry attempts on failure
char *new_buffer = sml_realloc(buffer, 20, 2);
if (new_buffer == NULL) {
// Reallocation failed, but 'buffer' is still valid
free(buffer);
} else {
// Reallocation succeeded, 'buffer' is now invalid. Use 'new_buffer'.
strcat(new_buffer, " world!");
free(new_buffer);
}
sml_realloc_change_place_bytes
A “copy-and-free” reallocation function that guarantees the new memory block is at a different location.
void *sml_realloc_change_place_bytes(void *ptr, unsigned long long old_size_in_bytes, unsigned long long new_size_in_bytes);
- Description: This function allocates a new memory block of
new_size_in_bytes
, copies the data from the oldptr
, and then frees the oldptr
. - Use Case: Useful when you need to be certain that the reallocated memory block is new and not an in-place expansion, which can be important for certain data structures or when interfacing with other systems.
- Return Value: A pointer to the new memory block, or
NULL
on failure.
sml_realloc_change_place_elements
Similar to the function above, but works with element counts instead of raw bytes.
void *sml_realloc_change_place_elements(void *ptr, unsigned long long old_num_elements, unsigned long long element_size, unsigned long long new_num_elements);
- Description: Allocates a new, zero-initialized block for
new_num_elements
, copies the old data, and frees the original pointer. - Safety: Includes overflow checks for both the old and new size calculations.
Use Cases and Examples
Use Case 1: Safely Creating a Dynamic Array
Scenario: You need to create an array of structs, but the size is determined at runtime. You want to ensure the allocation is safe from integer overflows and the memory is clean.
Solution: Use sml_calloc
.
typedef struct {
int id;
char name[50];
} User;
User* create_user_list(size_t num_users) {
// sml_calloc checks for overflow and zeroes the memory.
// Each user struct will have its fields initialized to 0/NULL.
User *user_list = sml_calloc(num_users, sizeof(User));
if (user_list == NULL) {
fprintf(stderr, "Failed to create user list for %zu users.\n", num_users);
return NULL;
}
// The list is ready to be populated.
return user_list;
}
Use Case 2: Resizing a Buffer with sml_realloc
Scenario: You are reading data from a file or network into a buffer. You don’t know the final size in advance, so you need to grow the buffer as more data arrives.
Solution: Use sml_realloc
.
char* read_all_data(FILE* stream) {
size_t capacity = 1024; // Initial buffer size
size_t size = 0;
char *buffer = sml_malloc(capacity);
if (!buffer) return NULL;
int c;
while ((c = fgetc(stream)) != EOF) {
if (size >= capacity - 1) { // -1 for the null terminator
// Double the capacity
size_t new_capacity = capacity * 2;
char *new_buffer = sml_realloc(buffer, new_capacity, 0);
if (new_buffer == NULL) {
fprintf(stderr, "Failed to grow buffer.\n");
free(buffer);
return NULL;
}
buffer = new_buffer;
capacity = new_capacity;
}
buffer[size++] = (char)c;
}
buffer[size] = '\0';
return buffer;
}
Use Case 3: Reallocating with a Guaranteed New Memory Block
Scenario: You have a hash table implementation where pointers to existing items must remain valid even after the table is resized. Standard realloc
might move the memory block, invalidating all existing pointers.
Solution: Use sml_realloc_change_place
to create a new table, and then manually re-insert or re-hash all items.
// Simplified hash table resize logic
void resize_hash_table(HashTable *table) {
size_t old_capacity = table->capacity;
size_t new_capacity = old_capacity * 2;
// Use change_place to get a completely new block for the table buckets.
// The old `table->buckets` will be freed automatically.
Bucket *new_buckets = sml_realloc_change_place_elements(
table->buckets,
old_capacity,
sizeof(Bucket),
new_capacity
);
if (new_buckets == NULL) {
// Handle catastrophic failure: the old table is gone, but new one couldn't be made.
// In a real app, you might try to recover or exit gracefully.
fprintf(stderr, "Hash table resize failed!\n");
table->buckets = NULL; // Mark as invalid
table->capacity = 0;
return;
}
table->buckets = new_buckets;
table->capacity = new_capacity;
// IMPORTANT: After getting the new memory, you must re-hash all old elements
// into the new, larger bucket array.
rehash_all_elements(table, old_capacity);
}