Skip to content

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 use sml_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 cause sml_malloc or sml_calloc to always return NULL, 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 if size 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 of sizeof_mem size. The allocated memory is initialized to zero.
  • Safety: Includes an integer overflow check on how_much * sizeof_mem. Returns NULL 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 or sml_calloc depending on the build mode.
  • Behavior:
  • In a release build (no DEBUG flag), it acts like sml_malloc(how_much * sizeof_mem) for performance.
  • In a debug build (-DDEBUG), it acts like sml_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 to new_requested_size. The contents will be unchanged up to the minimum of the old and new sizes.
  • Standard Behavior:
  • If ptr is NULL, it behaves like sml_malloc(new_requested_size).
  • If new_requested_size is 0, it behaves like free(ptr) and returns NULL.
  • 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 of 0 means it will try only once.
  • Return Value: A pointer to the reallocated memory, or NULL on failure. If it fails, the original ptr 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 old ptr, and then frees the old ptr.
  • 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);
}