Return of Stack Variable Address
Description
Return of Stack Variable Address occurs when a function returns a pointer or reference to a local variable that ceases to exist when the function returns. Local (automatic) variables are allocated on the stack and are deallocated when the function exits. Returning their address provides a dangling pointer—the memory is reclaimed and may be reused for other purposes. Any subsequent access through this pointer reads or writes memory that no longer belongs to the variable.
Risk
Using a dangling pointer to stack memory causes undefined behavior, crashes, and security vulnerabilities. The memory may contain garbage data, causing information disclosure of stack contents. If the stack memory is reused (by another function call), writes through the dangling pointer corrupt the new function's data or return addresses, enabling code execution. The bug is particularly dangerous because it may work during testing (if the memory isn't reused) but fail unpredictably in production.
Solution
Never return pointers or references to local variables. Use static or global variables if the data must persist (with thread-safety considerations). Allocate memory dynamically (heap) and return that pointer (caller must free). Pass a caller-provided buffer to fill. Return by value for small objects. In C++, use smart pointers or return std::optional/std::variant for optional results. Enable compiler warnings (-Wreturn-local-addr).
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Memory Corruption Writing through dangling pointer corrupts stack memory. |
| Confidentiality | Scope: Information Disclosure Reading dangling pointer exposes stack contents. |
| Availability | Scope: Crash/Code Execution Stack corruption can crash or enable code execution. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Return address of local variable
char* get_greeting_vulnerable() {
char greeting[100] = "Hello, World!";
return greeting; // BUG! Returns address of local variable!
}
void caller_vulnerable() {
char* msg = get_greeting_vulnerable();
printf("%s\n", msg); // Undefined behavior! Stack may be reused!
}
// VULNERABLE: Return pointer to local
int* get_value_vulnerable() {
int value = 42;
return &value; // BUG! value ceases to exist!
}
// VULNERABLE: Return address of local array element
int* get_element_vulnerable(int index) {
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
return &array[index]; // BUG! array is local!
}
// VULNERABLE: Return pointer from conditional
char* get_name_vulnerable(int id) {
if (id == 1) {
char name[] = "Alice";
return name; // BUG! local variable!
} else {
char name[] = "Bob";
return name; // BUG! both branches return local!
}
}
// VULNERABLE: Struct with pointer to local
struct Result {
int* data;
int size;
};
struct Result process_vulnerable(int input) {
int local_data[5];
struct Result result;
for (int i = 0; i < 5; i++) {
local_data[i] = input + i;
}
result.data = local_data; // BUG! Points to local!
result.size = 5;
return result; // result.data is dangling!
}
// VULNERABLE: Output parameter set to local address
void get_config_vulnerable(int** out_config) {
int config_values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
*out_config = config_values; // BUG! Sets to local address!
}
// VULNERABLE: Return reference in wrapper function
char* format_error_vulnerable(int code) {
char buffer[256];
snprintf(buffer, sizeof(buffer), "Error: %d", code);
return buffer; // BUG! buffer is local!
}
// VULNERABLE: C++ reference to local
int& get_reference_vulnerable() {
int local = 42;
return local; // BUG! Reference to local!
}
// VULNERABLE: Return address of local object
std::string* get_string_vulnerable() {
std::string local = "Hello";
return &local; // BUG! Object destroyed on return!
}
// VULNERABLE: Lambda capturing local by reference
auto get_lambda_vulnerable() {
int local = 42;
return [&local]() { return local; }; // BUG! local is gone!
}
// VULNERABLE: Iterator to local container
std::vector<int>::iterator get_iterator_vulnerable() {
std::vector<int> local = {1, 2, 3};
return local.begin(); // BUG! vector destroyed!
}
// VULNERABLE: Span to local array (C++20)
std::span<int> get_span_vulnerable() {
int local[] = {1, 2, 3, 4, 5};
return std::span{local}; // BUG! local array gone!
}
// VULNERABLE: Reference member initialized with local
class Vulnerable {
int& ref;
public:
Vulnerable(int& r) : ref(r) {}
};
Vulnerable create_vulnerable() {
int local = 42;
return Vulnerable(local); // BUG! ref dangles!
}
// VULNERABLE: String_view to local string
std::string_view get_view_vulnerable() {
std::string local = "Hello";
return local; // BUG! Implicit conversion to view of destroyed string!
}
// VULNERABLE: Common buffer pattern mistake
char* concat_vulnerable(const char* a, const char* b) {
char buffer[256];
snprintf(buffer, sizeof(buffer), "%s%s", a, b);
return buffer; // BUG! Returning local!
}
// VULNERABLE: getenv-like function with local storage
char* get_env_value_vulnerable(const char* name) {
char buffer[256];
if (getenv_safe(name, buffer, sizeof(buffer))) {
return buffer; // BUG! Local buffer!
}
return NULL;
}
Fixed Code
// SAFE: Return by value (for small data)
int get_value_safe() {
int value = 42;
return value; // Returns copy, not address
}
// SAFE: Use static storage (be careful with thread safety!)
const char* get_greeting_static() {
static char greeting[] = "Hello, World!";
return greeting; // Static lives for program lifetime
}
// SAFE: Dynamically allocate (caller must free)
char* get_greeting_heap() {
char* greeting = malloc(100);
if (greeting) {
strcpy(greeting, "Hello, World!");
}
return greeting; // Caller must free()!
}
// SAFE: Caller provides buffer
void get_greeting_buffer(char* buffer, size_t size) {
snprintf(buffer, size, "Hello, World!");
}
// Caller usage:
void caller_safe() {
char buffer[100];
get_greeting_buffer(buffer, sizeof(buffer));
printf("%s\n", buffer);
}
// SAFE: Return pointer to element of static array
int* get_element_safe(int index) {
static int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
if (index >= 0 && index < 10) {
return &array[index];
}
return NULL;
}
// SAFE: Use string literal (has static storage)
const char* get_name_literal(int id) {
if (id == 1) {
return "Alice"; // String literals have static storage
} else {
return "Bob";
}
}
// SAFE: Struct with dynamically allocated data
struct Result process_safe(int input) {
struct Result result;
result.data = malloc(5 * sizeof(int));
if (result.data == NULL) {
result.size = 0;
return result;
}
for (int i = 0; i < 5; i++) {
result.data[i] = input + i;
}
result.size = 5;
return result; // Caller must free result.data
}
// SAFE: Caller-provided output buffer
int get_config_safe(int* out_config, size_t max_size) {
int config_values[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
size_t count = sizeof(config_values) / sizeof(config_values[0]);
if (max_size < count) {
return -1; // Buffer too small
}
memcpy(out_config, config_values, count * sizeof(int));
return (int)count;
}
// SAFE: Allocate error message
char* format_error_safe(int code) {
char* buffer = malloc(256);
if (buffer) {
snprintf(buffer, 256, "Error: %d", code);
}
return buffer; // Caller must free
}
// SAFE: Return by value
int get_value_cpp_safe() {
int local = 42;
return local; // Copy returned
}
// SAFE: Return string by value
std::string get_string_safe() {
std::string local = "Hello";
return local; // Copy or move semantics
}
// SAFE: Return smart pointer
std::unique_ptr<std::string> get_string_smart() {
return std::make_unique<std::string>("Hello");
}
// SAFE: Lambda captures by value
auto get_lambda_safe() {
int local = 42;
return [local]() { return local; }; // Captures copy
}
// SAFE: Return container by value
std::vector<int> get_vector_safe() {
return {1, 2, 3}; // Returned by value
}
// SAFE: Return optional for maybe-results
std::optional<int> get_optional_safe(bool valid) {
if (valid) {
int local = 42;
return local; // Copies value into optional
}
return std::nullopt;
}
// SAFE: Use shared_ptr for shared ownership
std::shared_ptr<Resource> create_resource_safe() {
return std::make_shared<Resource>();
}
// SAFE: Move semantics for efficiency
std::string create_string_safe() {
std::string local = "Hello";
local += " World";
return local; // Move semantics (RVO/NRVO)
}
// SAFE: span to member or static data
class SafeContainer {
std::array<int, 5> data = {1, 2, 3, 4, 5};
public:
std::span<int> get_data() {
return data; // data is member, lives with object
}
};
// SAFE: String allocated on heap, view returned
class StringHolder {
std::string stored;
public:
void set(std::string s) {
stored = std::move(s);
}
std::string_view get() const {
return stored; // View of member string, safe while object exists
}
};
// SAFE: Factory pattern with proper ownership
class Resource {
public:
static std::unique_ptr<Resource> create() {
return std::make_unique<Resource>();
}
};
Exploited in the Wild
Stack Buffer Reuse Exploits
Attackers have exploited dangling pointers to stack memory by calling functions that reuse the stack space, then manipulating the data through the dangling pointer.
Information Disclosure via Stack
Reading through dangling stack pointers has exposed sensitive data like function arguments, return addresses, and local variables from other functions.
Return Address Overwrite
Writing through dangling stack pointers has been used to overwrite return addresses, enabling arbitrary code execution.
Tools to test/exploit
-
GCC/Clang -Wreturn-local-addr — warns about returning local addresses.
-
AddressSanitizer — detects use-after-return.
-
Valgrind — detects invalid memory access.
-
Coverity — static analysis for dangling pointers.
CVE Examples
-
CVE-2015-0235 — GHOST vulnerability involving buffer issues.
-
Multiple CVEs involving stack buffer reuse and dangling pointers.
-
Various use-after-return vulnerabilities in browsers and system software.
References
-
MITRE. "CWE-562: Return of Stack Variable Address." https://cwe.mitre.org/data/definitions/562.html
-
CERT C. "DCL30-C: Declare objects with appropriate storage durations." https://wiki.sei.cmu.edu/confluence/display/c/