The Guide to Safe & Modern C Memory Allocation Strategy

C gives you power and footguns to shoot yourself in the foot if you use it wrong.
This note standardizes how we allocate, initialize, copy, and free memory and strings in a portable, safe way (ISO C + small, documented helpers).
We avoid undefined behavior (UB), platform gotchas, and “hidden” memory allocation traps.

0) TL;DR Rules

  • Allocation ≠ Initialization. Never read uninitialized memory.
  • Prefer calloc for structs; else malloc + explicit init (memset or field-wise).
  • After realloc, init the new tail (bytes beyond old size).
  • Never use raw strdup in our codebase. Use xstrdup/xstrndup below.
  • Centralize lifetime with create/destroy APIs. Document ownership (borrowed vs owned).
  • After free, set pointer to NULL.

1) Core Allocation APIs

Function Purpose Initialized? Must Init?
malloc(n) Allocate n bytes (heap) ❌ garbage ✅ memset or assign
calloc(c,n) Allocate c*n bytes (heap) ✅ zeroed
realloc(p,n) Resize block p to n bytes 🔸 partial¹ ✅ init new tail
alloca(n) Allocate n bytes (stack, auto free) ❌ garbage ✅ memset or assign

¹ realloc: preserves old bytes; new bytes are uninitialized.

Golden rule: Never evaluate memory you didn’t initialize.

2) Safe Allocation Wrappers (ISO C, OOM-fatal, OOM-NULL with *_try variants)

Use these consistent behavior:

#ifndef SAFE_ALLOC_H
#define SAFE_ALLOC_H

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>

/* Abort-on-OOM (Out of Memory) policy (common for daemons/servers).
   If you need non-fatal OOM, use *_try variants separately. */

static inline void *xmalloc(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        fprintf(stderr, "FATAL: malloc(%zu) failedn", size);
        abort();
    }
    return p;
}

static inline void *xmalloc_try(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        return NULL;
    }
    return p;
}

static inline void *xcalloc(size_t nmemb, size_t size) {
    /* basic overflow guard */
    if (size && nmemb > SIZE_MAX / size) {
        fprintf(stderr, "FATAL: calloc overflow (%zu,%zu)n", nmemb, size);
        abort();
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        fprintf(stderr, "FATAL: calloc(%zu,%zu) failedn", nmemb, size);
        abort();
    }
    return p;
}

static inline void *xcalloc_try(size_t nmemb, size_t size) {
    /* basic overflow guard */
    if (size && nmemb > SIZE_MAX / size) {
        return NULL;
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        return NULL;
    }
    return p;
}


static inline void *xrealloc(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        fprintf(stderr, "FATAL: realloc(%p,%zu) failedn", ptr, size);
        abort();
    }
    return p;
}

static inline void *xrealloc_try(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        return NULL;
    }
    return p;
}

/* Free + NULL 
*  We put scope in here to avoid dangling if-else on non braces statement
*/
#define xfree(p) { if ((p) != NULL) { free(p); p = NULL; } }

/* Bounded str duplicate: must provide valid cstr with NUL-terminate. */
static inline char *xstrdup(const char *s) {
    if (!s) {
        /* Define our policy: duplicate NULL → empty string */
        char *z = xmalloc(1);
        z[0] = '';
        return z;
    }
    size_t n = strlen(s);
    /* +1 checked overflow */
    if (n >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrdup overflown");
        abort();
    }

    char *p = xmalloc(n + 1);
    memcpy(p, s, n + 1); /* includes '' */
    return p;
}

/* Bounded str duplicate: copy at most n bytes, n is size-1 (withouth NUL-terminate). */
static inline char *xstrndup(const char *s, size_t n) {
    if (!s) {
        char *z = xmalloc(1);
        z[0] = '';
        return z;
    }

    size_t m = 0;

    size_t m = strnlen(s, maxlen);
    if (m >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrndup overflown");
        abort();
    }

    char *p = xmalloc(m + 1);

    if (m){
        memcpy(p, s, m);
    }

    p[m] = '';
    return p;
}

/* Binary memcopy helper. */
static inline void *xmemcpy(const void *src, size_t n) {
    if (!src && n) {
        return NULL;
    }

    void *p = xmalloc(n ? n : 1);
    if (n){ 
        memcpy(p, src, n);
    }

    return p;
}

#endif /* SAFE_ALLOC_H */

Why not use strdup directly?

  • Our xstrdup checks for overflow and defines behavior for NULL input.
  • No surprises and consistent with our OOM policy.

3) Initialization Patterns

Structs (configs/contexts)

typedef struct {
    const char *host;
    int         port;
    int         backlog;
    void       *user_data;
} server_config_t;

/* Prefer calloc for zero/NULL defaults */
server_config_t *cfg = xcalloc(1, sizeof *cfg);
cfg->host   = xstrdup("0.0.0.0"); 
cfg->port   = 9000;
cfg->backlog= 256;

/* Or: malloc + memset + field init */
server_config_t *cfg2 = xmalloc(sizeof *cfg2);
memset(cfg2, 0, sizeof *cfg2);
cfg2->port = 9000;

xfree(cfg->host);
xfree(cfg);
xfree(cfg2);

realloc tail must be initialized

size_t old_cap = 16, new_cap = 64;
char *buf = xmalloc(old_cap);
/* ...write up to old_cap... */

buf = xrealloc(buf, new_cap);
/* New region [old_cap, new_cap) is garbage → initialize if you will read it */
memset(buf + old_cap, 0, new_cap - old_cap);

4) Ownership Conventions (VERY IMPORTANT)

Define clearly who allocates, who frees.

  • Borrowed pointer: points to memory we do not free (e.g., literals, caller-owned).
  • Owned pointer: allocated here, must be freed in destroy.
  • Copy-on-set: safest—API duplicates input and manages it.

Example API

typedef struct server server_t;

server_t *server_create(void);
void      server_destroy(server_t *s);

int        server_set_host(server_t *s, const char *host); /* copies */
const char*server_get_host(const server_t *s);             /* borrowed */

Implementation:

struct server {
    char  *host;   /* owned */
    int    port;
    size_t backlog;
};

server_t *server_create(void) {
    server_t *s = xcalloc(1, sizeof(server_t));
    s->port = 9000;
    s->backlog = 256;
    return s;
}

int server_set_host(server_t *s, const char *host) {
    char *copy = xstrdup(host);  /* safe */
    xfree(s->host);
    s->host = copy;
    return 0;
}

const char *server_get_host(const server_t *s) {
    return s->host ? s->host : "";
}

void server_destroy(server_t *s) {
    if (!s) return;
    xfree(s->host);
    xfree(s);
}

5) Create/Destroy + Heap Initialization

typedef struct {
    const char *host;
    int         port;
    int         backlog;
    size_t      read_chunk;
} uvsvr_config_t;

uvsvr_config_t uvsvr_config_defaults(void) {
    return (uvsvr_config_t){
        /* AS MOST C Library API always stated if char *p must be allocated, not literal*/
        .host        = xstrdup("0.0.0.0"), 
        .port        = 9000,
        .backlog     = 256,
        .read_chunk  = 16 * 1024,
    };
}

uvsvr_config_t *uvsvr_config_new(void) {
    uvsvr_config_t *p = xmalloc(sizeof *p);
    *p = uvsvr_config_defaults();
    return p;
}

6) Anti-Patterns (Don’t Do These)

  • Reading before init:
  T *p = xmalloc(sizeof(*p));
  if (p->flag) { /* UB */ }
  • Assuming realloc clears new bytes.
  • Freeing borrowed pointers, or leaking owned ones.
  • Using alloca for large/variable buffers.
  • Using unsafe string funcs (strcpy, strcat, legacy strncpy).

7) Practical Snippets

Safe formatting

char buf[64];
int n = snprintf(buf, sizeof(buf), "%s", input);
if (n < 0) { /* format error */ }
if ((size_t)n >= sizeof(buf)) { /* truncated */ }

8) Checklist

  • [ ] Use xcalloc for structs; or xmalloc + memset.
  • [ ] Always init after malloc/alloca.
  • [ ] After realloc, init new bytes.
  • [ ] Use xstrdup/xstrndup (never avoid strdup).
  • [ ] Centralize lifetime in create/destroy.
  • [ ] Define ownership in headers.
  • [ ] xfree after use → pointer = NULL.
  • [ ] Avoid alloca in production servers.
  • [ ] Prefer snprintf/memcpy with explicit sizes.

Summary:

  • malloc/alloca = garbage → must init.
  • calloc = zeroed defaults.
  • realloc = old preserved, new garbage.
  • Use safe wrappers (xmalloc, xcalloc, xrealloc, xstrdup, etc.).
  • Centralize allocation in create/destroy APIs.
  • Document the ownership contracts.
  • This avoids UB, leaks, dangling pointers, and footguns/shooting yourself in the foot.

Similar Posts