2

I’m working on a C project where I need to know if a pointer refers to memory allocated on the stack or the heap.

For example:

int x = 42; // stack variable int *y = malloc(sizeof(int)); // heap variable 

I tried writing my own strcat function that reallocates memory if the destination buffer is too small:

char* strcat(char* dest, const char* src, size_t destSize) { int srcLength = strlen(src); int destLength = strlen(dest); if (srcLength + destLength + 1 > destSize) { printf("Size exceeded, dynamically allocating memory!!\n"); size_t newSize = srcLength + destLength + 1; dest = (char *) realloc(dest, newSize * sizeof(char)); } char* tmp = dest; while(*tmp != '\0') { tmp++; } while(*src != '\0') { *tmp = *src; tmp++; src++; } *tmp = '\0'; return dest; } 

The problem: if dest was allocated on the stack (e.g., char buffer[10];), then calling realloc(dest, …) crashes with realloc(): invalid pointer.

What I expected

I was hoping there might be a way to check whether dest is stack-allocated or heap-allocated before deciding whether to call realloc.

My question

Is there any reliable way in C to determine if a pointer refers to stack memory vs heap memory? Or is this simply not possible in standard C, and I should always ensure that only heap memory is passed to functions like this?

23
  • 6
    Why do you need to know? Sounds like a design issue. Commented Sep 1 at 14:02
  • 2
    "stack" is not a term of the C standard. This is a implementation detail. Hence, the standard C library does not have what you want to have. But, you can look for it in extended API like POSIX or Win32. Commented Sep 1 at 14:11
  • 7
    Even if you are passed a pointer to dynamically allocated memory (“on the heap”), it is not necessarily the start of an allocated block. Somebody could allocate char *c = malloc(1024); and pass you c + 323. Or they could do struct foo *p = malloc(sizeof *p); and pass you &p->string. Attempting to realloc such a pointer would cause problems. Commented Sep 1 at 14:11
  • 2
    "I was hoping there might be a way to check whether dest is stack-allocated or heap-allocated before deciding whether to call realloc." Don't do that, it's not something your program should check in run-time. If you get a crash during development: good, now fix the root problem which is the bug you've written elsewhere, rather than fixing symptoms of the bug, ie crashes when calling free(). Commented Sep 1 at 14:13
  • 2
    Further, this cannot be done without coordination in the caller. In your example, you apparently expect the caller to use the returned address and never again to use the argument they passed for dest. If you are going to make a routine that requires a contract with the caller like this, then simply also require the caller to pass an argument indicating whether or not the memory may be reallocated. Or design the routine to always expect reallocatable memory and require the caller to pass that. Or return an error if the destination is too small and let the caller deal with it. Commented Sep 1 at 14:14

3 Answers 3

2

There's no way to determine if a pointer can be safely passed to realloc. It's up to the programmer to not pass such pointers to your strcat.

Also, you shouldn't name your function strcat if it's not compatible with strcat. It's not just the extra argument, it's the fact that your function invalidates the pointer passed as the first argument. strcat doesn't do that.

Sign up to request clarification or add additional context in comments.

3 Comments

I suggest "ISO C does not define a way" as more supportable than "There's no way". But for all practical intents and purposes, this is the answer either way.
Nah. I'm not going to pretend they asked about a certain version of a specific compiler on a specific hardware on a specific version of a specific OS. They asked about C. /// And note that my answer said "safe to pass to realloc", which is not the same as "is on the heap". If I have to concede it's possible, I'd have to go in a lot more details about that, and it's simply not worth it.
Worth noting that functions beginning with the trigraph “str” are reserved by the C Standard itself (though the name str alone is not reserved, lol). It is also a generally bad idea to redefine any C standard library function name, as the linker will wreak vengeance upon you.
1

Rather than try to determine the memory locations of the pointers passed to the function, let the function allocate memory and return a pointer to the allocated memory. The caller can then decide what to do.
Here string literals are passed to the function. The caller prints the combined string and frees the memory.

#include <stdio.h> #include <stdlib.h> #include <string.h> char *strcombine ( char *first, char *second) { char *both = NULL; if ( NULL == ( both = malloc ( 1 + strlen ( first) + strlen ( second)))) { fprintf ( stderr, "problem malloc\n"); return NULL; } char *tmp = both; while ( *first) { *tmp = *first; ++tmp; ++first; } while ( *second) { *tmp = *second; ++tmp; ++second; } *tmp = 0; return both; } int main ( void) { char *begin = "abcdefghij"; char *end = "klmnopqrstuvwxyz1234567890"; char *all = NULL; if ( NULL != ( all = strcombine ( begin, end))) { puts ( all); free ( all); } } 

If begin has been allocated, this is one way to handle that.

#include <stdio.h> #include <stdlib.h> #include <string.h> char *strcombine ( char *first, char *second) { char *both = NULL; if ( NULL == ( both = malloc ( 1 + strlen ( first) + strlen ( second)))) { fprintf ( stderr, "problem malloc\n"); return NULL; } char *tmp = both; while ( *first) { *tmp = *first; ++tmp; ++first; } while ( *second) { *tmp = *second; ++tmp; ++second; } *tmp = 0; return both; } int main ( void) { char *begin = NULL; char *end = "klmnopqrstuvwxyz1234567890"; char *all = NULL; if ( NULL == ( begin = malloc ( 20))) { fprintf ( stderr, "problem malloc begin\n"); return 1; } strcpy ( begin, "abcdefghij"); if ( NULL != ( all = strcombine ( begin, end))) { free ( begin); begin = all; puts ( begin); } free ( begin); } 

2 Comments

A reasonable idea with char *strcombine ( char *first, char *second) {, yet more useful as char *strcombine ( const char *first, const char *second) {. (Greater application usage and potential performance improvements).
Candidate alternative based on your good idea: char* str_combine_alt(const char *first, const char *second) { size_t l1 = strlen(first); size_t l2 = strlen(second); if (l1 >= SIZE_MAX - l2) { return NULL; } char *dest = malloc(l1 + l2 + 1); if (dest == NULL) { return NULL; } char *p = dest; while (*first) { *p++ = *first++; } do { *p++ = *second; } while (*second++); return dest; }
0

Is there any reliable way in C to determine if a pointer refers to stack memory vs heap memory? Or is this simply not possible in standard C, and I should always ensure that only heap memory is passed to functions like this?

  1. C language does not know about the stack or heap. It knows only objects storage durations
  2. There is no portable way of checking it.
  3. If you want to stick to one toolchain / linker script / OS you can do this. Here is an expample:
 #define _GNU_SOURCE 1 #include <stdio.h> #include <stdbool.h> #include <stdint.h> #include <string.h> #include <pthread.h> #include <unistd.h> // sbrk() // Linker symbols for classic brk-heap extern char end; extern char _end; // ------------------------------------- // /proc/self/maps scanner & classifier // ------------------------------------- typedef enum { PTRMAP_STACK, // [stack] or [stack:<tid>] PTRMAP_HEAP_GLIBC, // [heap] (the main glibc/brk heap mapping) PTRMAP_HEAP_ANON_RW, // anonymous, private, readable+writeable mmap (heap-like arena) PTRMAP_FILE_MAPPED, // file-backed mapping PTRMAP_OTHER, // anything else PTRMAP_NOT_FOUND // not inside any mapping (unlikely if ptr is valid) } ptrmap_class_t; // Trim trailing newline/spaces in-place. static void trimLine(char *s) { if(!s) return; size_t n = strlen(s); while(n && (s[n-1] == '\n' || s[n-1] == '\r' || s[n-1] == ' ' || s[n-1] == '\t')) { s[--n] = '\0'; } } // Classify a pointer by scanning /proc/self/maps once. // Fast path: returns as soon as it finds the covering mapping. static ptrmap_class_t classifyPointerProcMaps(const void *ptr, char *outLine, size_t outLineCap) { if(outLine && outLineCap) { outLine[0] = '\0'; } #if !defined(__linux__) (void)ptr; (void)outLine; (void)outLineCap; return PTRMAP_NOT_FOUND; #else FILE *fp = fopen("/proc/self/maps", "r"); if(!fp) { return PTRMAP_NOT_FOUND; } // Buffer large enough for typical maps lines. char line[1024]; const uintptr_t P = (uintptr_t)ptr; ptrmap_class_t result = PTRMAP_NOT_FOUND; while(fgets(line, sizeof(line), fp)) { // Example line: // 00400000-0040c000 r-xp 00000000 08:02 131073 /bin/cat // 0060c000-0060d000 r--p 0000c000 08:02 131073 /bin/cat // 0060d000-0060e000 rw-p 0000d000 08:02 131073 /bin/cat // 00e2d000-0101e000 rw-p 00000000 00:00 0 [heap] // 7ffcd8b0b000-7ffcd8b2c000 rw-p 00000000 00:00 0 [stack] // // Parse start-end, perms, offset, dev, inode; then get pathname (if any). unsigned long startUL = 0, endUL = 0, offUL = 0; char perms[8] = {0}; unsigned int devMajor = 0, devMinor = 0; unsigned long inode = 0; int nconsumed = 0; // %n stores number of chars consumed so far so we can grab the path. int scanned = sscanf(line, "%lx-%lx %7s %lx %x:%x %lu %n", &startUL, &endUL, perms, &offUL, &devMajor, &devMinor, &inode, &nconsumed); if(scanned < 7) { continue; } const uintptr_t lo = (uintptr_t)startUL; const uintptr_t hi = (uintptr_t)endUL; if(!(P >= lo && P < hi)) { continue; // pointer not in this mapping } // Pointer lies in this mapping: classify it. const bool r = (perms[0] == 'r'); const bool w = (perms[1] == 'w'); const bool x = (perms[2] == 'x'); const bool pflag = (perms[3] == 'p'); // private (copy-on-write) (void)r; (void)x; // r/x not strictly needed for classification // Extract (optional) pathname const char *path = NULL; if(nconsumed > 0 && (size_t)nconsumed < strlen(line)) { // Skip spaces to the pathname token char *p = line + nconsumed; while(*p == ' ' || *p == '\t') { ++p; } trimLine(p); path = (*p ? p : NULL); } // Save the raw line if requested if(outLine && outLineCap) { // Copy original (trimmed) line for diagnostics strncpy(outLine, line, outLineCap - 1); outLine[outLineCap - 1] = '\0'; trimLine(outLine); } // Identify special tags if(path && strncmp(path, "[stack", 6) == 0) // matches "[stack]" and "[stack:tid]" { result = PTRMAP_STACK; } else if(path && strcmp(path, "[heap]") == 0) { result = PTRMAP_HEAP_GLIBC; // the main (brk) heap mapping } else if((inode == 0) && (!path || path[0] == '\0') && pflag && w) { // Anonymous, private, writeable mapping: typical for mmap() arenas. result = PTRMAP_HEAP_ANON_RW; } else if(path && path[0] != '\0' && path[0] != '[') { result = PTRMAP_FILE_MAPPED; } else { result = PTRMAP_OTHER; } break; // done; we found the covering mapping } fclose(fp); return result; #endif } // ------------------------------------- // Classic brk-heap & current-thread stack helpers (from earlier snippet) // ------------------------------------- static bool isPointerOnHeapBrk(const void *ptr) { const uintptr_t heapStart = (uintptr_t)(&_end ? &_end : &end); const uintptr_t heapEnd = (uintptr_t)sbrk(0); if((void*)heapEnd == (void*)-1) { return false; } const uintptr_t p = (uintptr_t)ptr; return (p >= heapStart) && (p < heapEnd); } static bool isPointerOnStack_CurrentThread(const void *ptr) { #if defined(__linux__) pthread_attr_t attr; if(pthread_getattr_np(pthread_self(), &attr) != 0) { return false; } void *stackBase = NULL; size_t stackSize = 0; bool on = false; if(pthread_attr_getstack(&attr, &stackBase, &stackSize) == 0 && stackSize > 0) { const uintptr_t lo = (uintptr_t)stackBase; const uintptr_t hi = lo + stackSize; const uintptr_t p = (uintptr_t)ptr; on = (p >= lo) && (p < hi); } pthread_attr_destroy(&attr); return on; #else (void)ptr; return false; #endif } // ------------------------------------- // Unified classifier // ------------------------------------- typedef enum { PTR_CLASS_STACK, // current thread stack or [stack] mapping PTR_CLASS_HEAP_BRK, // classic brk() heap PTR_CLASS_HEAP_MMAP, // anonymous private RW mapping (heap-like arena) PTR_CLASS_FILE_MAPPED, // file-backed mapping PTR_CLASS_OTHER, // everything else / not found } ptr_class_t; static ptr_class_t classifyPointerExtended(const void *ptr) { // Prefer exact current-thread stack range: if(isPointerOnStack_CurrentThread(ptr)) { return PTR_CLASS_STACK; } // Fall back to maps tags: char line[1024]; const ptrmap_class_t m = classifyPointerProcMaps(ptr, line, sizeof(line)); (void)line; // (keep for debugging if desired) switch(m) { case PTRMAP_STACK: return PTR_CLASS_STACK; case PTRMAP_HEAP_GLIBC: return PTR_CLASS_HEAP_BRK; case PTRMAP_HEAP_ANON_RW: return PTR_CLASS_HEAP_MMAP; case PTRMAP_FILE_MAPPED: return PTR_CLASS_FILE_MAPPED; case PTRMAP_OTHER: case PTRMAP_NOT_FOUND: default: break; } // As a final check, also consider classic brk-heap range if(isPointerOnHeapBrk(ptr)) { return PTR_CLASS_HEAP_BRK; } return PTR_CLASS_OTHER; } #include <stdlib.h> #include <stdio.h> static const char *className(ptr_class_t c) { switch(c) { case PTR_CLASS_STACK: return "STACK"; case PTR_CLASS_HEAP_BRK: return "HEAP_BRK([heap])"; case PTR_CLASS_HEAP_MMAP: return "HEAP_MMAP(anon rw-p)"; case PTR_CLASS_FILE_MAPPED: return "FILE_MAPPED"; default: return "OTHER"; } } int main(void) { int stackVar = 0; void *small = malloc(1024); // often brk-heap void *large = malloc(4 * 1024 * 1024); // often mmap() heap arena printf("&stackVar -> %s\n", className(classifyPointerExtended(&stackVar))); printf("small malloc -> %s\n", className(classifyPointerExtended(small))); printf("large malloc -> %s\n", className(classifyPointerExtended(large))); static int staticVar = 0; printf("&staticVar -> %s\n", className(classifyPointerExtended(&staticVar))); free(small); free(large); return 0; } 

https://godbolt.org/z/5E8Tvzcvs

godbolt output:

&stackVar -> STACK small malloc -> HEAP_BRK([heap]) large malloc -> HEAP_MMAP(anon rw-p) &staticVar -> FILE_MAPPED 

Of course it cant guarantee that you can realloc of free pointer even if it is referencing heap area

4 Comments

Even this is problematic because it does not detect if the pointer was returned by malloc. For example, it's not safe to pass (char*) small + 10 to realloc.
I was answering OP direct question cited in my answer. He asked how to determine which memory region a pointer is referring to.
Yes, but it's important to understand that a literal answer to the question doesn't solve the real problem.
Added a note

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.