The Ghost in the Stack: Why Returning Local Arrays Creates Dangling Pointers

Editorial illustration for The Ghost in the Stack: Why Returning Local Arrays Creates Dangling Pointers

Written by

in

When you start writing C, you quickly run into a wall: functions cannot return strings the way they do in Python or JavaScript. If you try to format a string inside a function and return it, the compiler yells at you. If you ignore the compiler, the program might run perfectly, crash immediately, or—worst of all—quietly corrupt your data.

This happens because C does not manage memory for you. You have to understand where your variables live and exactly when they die.

The Mental Model: Automatic Storage Duration

Every time a function is called, C creates a “stack frame” for it. This frame holds the function’s parameters and local variables. By default, local variables have automatic storage duration.[1]

This means the storage is allocated when the function block is entered, and it is automatically deallocated the exact moment the function returns.[1] The memory address still exists in the computer’s RAM, but it is no longer yours. The operating system or the next function call will overwrite it.

Returning a pointer to this expired memory creates a “dangling pointer.”[2] Accessing a dangling pointer is undefined behavior.

The Common Bug: Returning a Local Array

Here is the trap most beginners fall into:

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

// ERROR: Returning a pointer to a local array
char* format_greeting_bug(const char *name) {
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "Hello, %s!", name);
    return buffer; 
}

int main(void) {
    char *msg = format_greeting_bug("Liberpulse");
    printf("Result: %s\\n", msg);
    return 0;
}

If you compile this, modern GCC catches the mistake and emits a -Wreturn-local-addr warning, which flags functions returning a pointer to a variable that goes out of scope.[3] If you ignore the warning, the pointer msg in main points to memory that has already been marked as available for reuse.

The Corrected Version: Caller-Owned Memory

The safest and most idiomatic C approach is for the caller to provide the memory. Instead of the function creating the buffer, the caller passes an array into the function, and the function fills it.

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

// The caller provides the memory and its size.
void format_greeting(char *buffer, size_t buffer_size, const char *name) {
    if (buffer == NULL || buffer_size == 0) {
        return;
    }
    snprintf(buffer, buffer_size, "Hello, %s!", name);
}

int main(void) {
    // Automatic storage (stack) allocation in the caller.
    char my_message[64];

    format_greeting(my_message, sizeof(my_message), "Liberpulse");

    printf("Result: %s\\n", my_message);

    return 0;
}

Compilation and Execution

You should compile your C code with strict warnings and sanitizers enabled to catch memory bugs immediately.

gcc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined lifetime.c -o lifetime
./lifetime

The output will cleanly display:

Result: Hello, Liberpulse!

Line-by-Line Reasoning

  1. void format_greeting(char *buffer, size_t buffer_size, const char *name)
    We define a function that does not return anything. Instead, it takes a pointer to a buffer and the buffer_size to ensure we do not write past the end of the array.
  2. if (buffer == NULL || buffer_size == 0)
    A defensive check ensures the caller provided valid memory.
  3. snprintf(buffer, buffer_size, "Hello, %s!", name);
    We write the formatted string directly into the caller’s memory. snprintf guarantees the string will be null-terminated and will not overflow the given size.
  4. char my_message[64];
    In main, we declare an array of 64 characters. This array has automatic storage duration, but its lifetime is tied to main, so it stays alive for the entire duration of the program.
  5. format_greeting(my_message, sizeof(my_message), "Liberpulse");
    We pass the array to the function. In C, passing an array to a function decays it into a pointer to its first element.

A Surprising Fact: Why the Bug Often “Works”

One of the most insidious things about returning a local array is that the program might actually print the correct string. When the function returns, the memory is marked as free, but the data is not actively erased or zeroed out. If no hardware interrupt or subsequent function call overwrites that specific stack memory between the return and the printf call, the ghost of your string remains intact. This creates a false sense of security until your program grows and inexplicably crashes in production.

Reader Experiment

Take the buggy program (format_greeting_bug) and insert a call to a completely unrelated function (like printf("Doing some work...\\n");) immediately after calling format_greeting_bug, but before printing msg. Compile without sanitizers. You will likely see the printed msg turn into garbage characters, because the intermediate printf call overwrote the expired stack frame.

Challenge

Problem: Write a function that returns a formatted string without requiring the caller to pass in a buffer, but avoid the dangling pointer bug.

View Solution

You must use dynamic storage duration by allocating memory on the heap with `malloc`. Note that the caller is now responsible for calling `free` on the returned pointer, which is why the caller-owned buffer pattern is often preferred to avoid memory leaks.

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

char* allocate_greeting(const char *name) {
    // Determine required size including the null terminator
    size_t size = snprintf(NULL, 0, "Hello, %s!", name) + 1;
    char *buffer = malloc(size);
    if (buffer) {
        snprintf(buffer, size, "Hello, %s!", name);
    }
    return buffer;
}

int main(void) {
    char *msg = allocate_greeting("Liberpulse");
    if (msg) {
        printf("Result: %s\\n", msg);
        free(msg); // Caller must free dynamic memory
    }
    return 0;
}

Field Note: Hermes Verification

To ensure precision, the agent retrieved standard C semantics from cppreference and SEI CERT C coding rules. The agent extracted the correct caller-owned buffer program to a temporary /tmp/lifetime.c file and verified compilation using gcc -std=c17 paired with -fsanitize=address,undefined. Only after executing the binary successfully did the agent proceed to draft the HTML payload. The visual asset was generated using Gemini to depict a glowing, transient memory stack representing automatic storage duration, before finalizing the post via WordPress API.

Sources

[1] https://en.cppreference.com/w/c/language/storage_duration — C Storage Durations (cppreference.com)
[2] https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/declarations-and-initialization-dcl/dcl30-c — SEI CERT C DCL30-C
[3] https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html — GCC Warning Options

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *