C / DYNAMIC MEMORY
Lifetime bugs: returning stack addresses from functions
Spot functions that return pointers into their own stack frame and replace them with caller buffers, static storage or by-value returns.
What you will learn
- Spot a dangling return: the address of any local or its members escaping upward.
- Explain why a dead stack pointer often prints correctly until the next call.
- Convert `return buf;` into a caller-supplied buffer with an explicit capacity.
- Catch the pattern with -Wreturn-local-addr and ASan stack-use-after-return.
Understanding Lifetime bugs: returning stack addresses from functions
An object declared inside a function without `static` has automatic storage duration: it starts existing when control enters the block and stops existing when control leaves it, and at that moment every pointer to it becomes indeterminate. In `char *f(void) { char buf[32]; ...; return buf; }` the array decays to a pointer, so what leaves the function is an address, not the bytes, and that address names storage the stack has already given up. Nothing erases the bytes on the way out; the frame is simply no longer reserved, so the next call is free to lay its own locals over exactly that region.
That is why the broken version so often prints the right string in a two-line test: between the return and the print, nothing has pushed a frame over the bytes yet. The first intervening call writes its own locals there, and printf is itself such a call, so the same source can start producing garbage after an unrelated edit, a different optimisation level or a different libc. Because the read is undefined behaviour, the compiler is also allowed to reason as if it never happens; GCC will in some cases substitute a null pointer for such a return value, turning the apparently harmless version into an immediate segfault in the caller.
The useful mental model is direction. An address may travel downward, into functions you call, because your frame outlives them, but it must never travel upward out of the frame that owns the object. That leaves three honest ways to hand data back: let the caller own the storage and pass a pointer plus its capacity, return the value itself (wrap an array in a struct so the return statement copies the bytes), or point at storage that outlives every call, such as a string literal, a `static` object or an allocated block whose ownership you document. The same rule covers the less obvious cases: `&arr[0]` for a local array, `&s.field` for a local struct, a local VLA and a compound literal written inside the function are all automatic storage.
<stdio.h>
/* The bug, kept here only so you can see its shape. gcc answers with
"function returns address of local variable [-Wreturn-local-addr]",
clang with "address of stack memory associated with local variable
'n' returned". Nothing in this program calls it. */
int *dangling(void)
{
int n = 42;
return &n; /* n stops existing at this return */
}
static int *watched; /* an escape route no warning covers */
static void watch(int *slot) { watched = slot; }
static void bump(int *slot) { *slot += 1; }
int main(void)
{
int hits = 0;
watch(&hits); /* fine: hits outlives the call to watch */
*watched += 1;
printf("through the global: hits = %d\n", hits);
bump(&hits); /* fine for the same reason */
printf("through the parameter: hits = %d\n", hits);
return 0; /* watched dangles from here on */
}
A pointer is valid only as long as the object it points at, and a function's locals cease to exist at the moment it returns.
Worked examples
The static buffer trap
Making the local static removes the lifetime bug but gives every caller the same bytes.
<stdio.h>
static const char *make_label(int id)
{
static char buf[16]; /* one buffer for the whole program */
snprintf(buf, sizeof buf, "sensor-%d", id);
return buf; /* legal: static storage outlives the call */
}
int main(void)
{
const char *a = make_label(1);
const char *b = make_label(2);
printf("a = %s\n", a);
printf("b = %s\n", b);
printf("same buffer: %s\n", a == b ? "yes" : "no");
return 0;
}
Example explained
Line 1`static char buf[16]` has static storage duration, so the returned pointer stays valid for the rest of the program and no lifetime rule is broken.
Line 2The second call formats into those same 16 bytes, so `a`, obtained before it, now reads sensor-2 as well.
Line 3`a == b` is true because the function has exactly one buffer to hand out; two independent results cannot coexist.
Line 4This is why `ctime` and `asctime` needed reentrant replacements: one shared static buffer is also a data race between threads.
Copy out, or let the caller own the bytes
Two returns with no lifetime problem: a struct returned by value, and a buffer supplied by the caller.
<stdio.h>
typedef struct { char text[16]; } Label;
static Label label_of(int id) /* the return copies the struct */
{
Label l;
snprintf(l.text, sizeof l.text, "sensor-%d", id);
return l;
}
static int fill_label(char *out, size_t cap, int id) /* caller owns the bytes */
{
return snprintf(out, cap, "sensor-%d", id);
}
int main(void)
{
Label a = label_of(1);
Label b = label_of(2);
char c[16];
int n = fill_label(c, sizeof c, 3);
printf("%s %s\n", a.text, b.text);
printf("%s (%d chars)\n", c, n);
return 0;
}
Example explained
Line 1`return l;` copies the whole struct into the caller's object, so `a` and `b` hold independent bytes and no pointer to the callee's frame exists.
Line 2C forbids an array return type outright, so wrapping the array in a struct is what lets the bytes travel by copy at all.
Line 3`fill_label` receives the capacity next to the pointer, so it cannot write past an object it does not own.
Line 4snprintf returns the length it wanted to write, which is how the caller detects truncation instead of guessing.
Important notes
Returning a pointer is not the problem; returning a pointer to automatic storage is. String literals and `static` objects have static storage duration, and handing back `&caller_array[i]` or an allocated block is fine.
-Wreturn-local-addr only recognises the literal `return &local;` shape. Route the address through a global, an out-parameter or a struct field and the warning disappears while the lifetime bug remains.
Common mistakes
Declaring the function correct because a one-line test printed the right string: the bytes survive only until the next call pushes a frame over them, so the failure surfaces later, inside unrelated code, and looks like corruption from somewhere else.
Changing `char buf[32]` to `static char buf[32]` just to silence the warning: the pointer becomes valid but there is one buffer per program, so printf("%s %s", f(1), f(2)) prints the same label twice and two threads overwrite each other.
Keeping a pointer into a struct returned by value, as in `char *p = make_tag().text;`: the temporary holding the return value dies at the end of that statement, so p already dangles on the next line.
Try it yourself
Change, predict, then run
Write `char *stamp(int n)` that formats "tick-%d" into a local char buf[16] and returns it, compile it and read the warning, then rewrite it as `void stamp(int n, char *out, size_t cap)` and print two different stamps that you hold at the same time.
Open the C workspaceCheck your understanding
A function formats text into a local char buf[32] and returns buf. The caller prints it immediately and the text is correct. What does that tell you?
- That returning a local array is safe as long as the caller uses the pointer before calling anything else.
- That the compiler promoted buf to static storage because it saw the address escape.
- Nothing: the frame just has not been reused yet, and any intervening call or optimisation level can change the result.
- That buf was small enough to live in a register, so no stack memory was involved.
Show answer
The read is undefined behaviour whatever it prints; the bytes are intact only because no new frame has been built over them yet. The first option is the tempting one, since using the value 'immediately' is exactly what makes the bug hide, but printf is itself a call that puts its own locals in that region, and the compiler may optimise on the assumption that such a return never happens.