Stack Overflow
Unbounded recursion pushes frames until the stack pointer crosses into a guard page the kernel deliberately left unmapped; the resulting fault is reported as SIGSEGV, "Maximum call stack size exceeded" or RecursionError depending on who catches it first.
The problem
recurse() calls recurse(). Each call pushes a frame and none returns. The stack is a finite region — what stops it, how does the program find out, and why does the same bug have three different error messages in C++, JavaScript and Python?Frames pile up
Each call to recurse executes a call (8 bytes for the return address) and a prologue that reserves its frame — say 32–64 bytes for a trivial function, kilobytes for one with local arrays. Nothing returns, so nothing is popped, and the stack pointer marches towards lower addresses at a rate of one frame per call. At 48 bytes per frame, an 8 MB stack holds about 170,000 frames; at 4 kB per frame, about 2,000. Both take well under a millisecond to exhaust.
The stack does not "run out" by hitting the heap. The region is placed at the top of the address space with an unmapped gap below it, and the kernel grows the mapping on demand as the stack pointer descends — up to a limit. That limit is the stack size: on Linux the main thread’s limit is ulimit -s, 8 MB by default; on Windows it is set in the executable header, 1 MB by default; on macOS the main thread gets 8 MB. Threads other than the main one get a stack allocated by the threading library — glibc’s pthread_create defaults to the ulimit -s value (8 MB), macOS gives secondary threads 512 kB, Windows gives them 1 MB, the JVM 1 MB (-Xss), and Go gives goroutines a few kilobytes that grow by copying the stack, which is why Go recursion rarely overflows.
1#include <cstdio>2long depth = 0;3void recurse() {4 char pad[64]; // make the frame visibly large5 pad[0] = (char)depth; // keep the compiler from optimising the frame away6 depth++;7 recurse(); // never returns; each call pushes another ~100 bytes8}9int main() { recurse(); }10// $ ./a.out → Segmentation fault (core dumped), depth ≈ 8 MB / frame sizeThe guard page
Below the maximum extent of the stack sits a guard page (or several): a page that is deliberately left unmapped or mapped with no permissions, so that the first access beyond the limit is a page fault the kernel cannot satisfy. That fault is the detection mechanism — there is no counter, no check on each call. The kernel’s page-fault handler sees an address just below the stack mapping, refuses to grow it further, and delivers SIGSEGV (User Mode vs Kernel Mode, Page Faults). The signal handler, if any, needs a stack to run on and the stack is what just failed, which is why runtimes that want to report overflows nicely register an alternate signal stack (sigaltstack) in advance.
The guard page works only if every frame touches the stack within one page of the previous one. A function with a 1 MB local array could skip the guard page entirely and land in whatever mapping lies below — a silent corruption instead of a crash. This is why compilers emit stack probes for large frames (touching every page of the frame in order, -fstack-clash-protection, always on in MSVC), and why the Stack Clash vulnerabilities of 2017 were fixed by widening the gap to 1 MB on Linux.
Secondary threads have no room to grow: their stack is a fixed mmap with a guard page at the bottom, so a thread that recurses overflows sooner than main would — 512 kB on macOS is only ~10,000 small frames — and a thread pool that runs deeply recursive user code is a common place to find this.
$ ulimit -s 8192 # kB: the main thread may grow to 8 MB $ grep -A1 stack /proc/self/maps 7ffd3a1c9000-7ffd3a1ea000 rw-p 00000000 00:00 0 [stack] # 132 kB mapped so far; grows on demand $ dmesg | tail -1 a.out[41217]: segfault at 7ffd39fc8ff8 ip 000055d0c3f0a161 sp 00007ffd39fc9000 error 6 in a.out # sp is exactly at the boundary; the faulting address is 8 bytes below it: the next push
What each runtime reports
C and C++: nothing catches the signal, so the process dies with "Segmentation fault", exit status 139 (128 + 11). The core dump shows tens of thousands of identical frames; gdb prints #0 recurse () … #171303 main (). A deep but finite recursion — a naive recursive descent parser on a long input, a recursive std::variant visit — shows the same symptom without an infinite loop.
JavaScript (V8): the engine checks the stack pointer against a limit on every function entry (an explicit comparison, not the guard page) and throws RangeError: Maximum call stack size exceeded as a catchable exception well before the OS limit — roughly 10,000–15,000 frames in Node with the default ~1 MB V8 stack, fewer with larger frames. node --stack-size=… raises the engine limit but not the thread’s real stack, so setting it too high converts the clean exception back into a segfault.
Python (CPython): the interpreter counts Python-level frames and raises RecursionError: maximum recursion depth exceeded at sys.getrecursionlimit(), 1,000 by default — a soft limit chosen to stay far inside the C stack, because before 3.11 every Python call was also a C-level recursion in the evaluator. Since 3.11 pure-Python calls no longer consume C stack, and 3.12 added a separate C-recursion limit; sys.setrecursionlimit(10**6) still lets you hit the real stack and get a genuine SIGSEGV if C extensions or the evaluator recurse underneath.
| Runtime | Detects by | Limit | Reports as | Catchable |
|---|---|---|---|---|
| C / C++ | Guard page → SIGSEGV | 8 MB main (Linux), 1 MB (Windows) | Segmentation fault, exit 139 | Only via sigaltstack handler |
| JavaScript (V8) | Stack-pointer check on entry | ~1 MB engine stack → ~10k frames | RangeError: Maximum call stack size exceeded | Yes |
| Python (CPython) | Frame counter | sys.getrecursionlimit() = 1000 | RecursionError | Yes |
| Java (HotSpot) | Guard page + handler | -Xss, 1 MB default | StackOverflowError | Yes (but rarely wise) |
| Go | Growable stacks | Up to 1 GB (64-bit) | fatal error: stack overflow | No |
Recursion in DSA and the explicit stack
Every recursive algorithm you wrote in Recursion & Backtracking, Depth-First Search (DFS) or Divide and Conquer carries a depth bound: linear in the input for a list traversal, the tree height for a tree walk, the recursion depth of quick-sort on adversarial input. A DFS on a 100,000-node path graph is 100,000 frames deep — fine in Go, fatal in Python at the default limit, borderline in Node, and a crash on a 512 kB macOS worker thread in C++. "Works on the test cases" and "overflows in production on a long chain" is one of the most common ways recursive code fails.
The fix is mechanical: a recursion is a loop with an explicit Stack of pending work. What the hardware stack held implicitly — the arguments and the point to resume — becomes a struct you push and pop yourself, on the heap, whose size is limited by memory rather than by a 1 MB region. DFS, tree traversals, flood fill and backtracking all convert this way; memoised recursion converts to Tabulation (Bottom-Up DP) by computing subproblems in dependency order. The trade is readability for a hard depth guarantee, and the conversion is worth doing whenever the depth depends on input you do not control.
1function dfsRecursive(u: number, g: number[][], seen: boolean[]): void {2 seen[u] = true;3 for (const v of g[u]) if (!seen[v]) dfsRecursive(v, g, seen); // one frame per level4}5 6function dfsIterative(start: number, g: number[][]): boolean[] {7 const seen = new Array<boolean>(g.length).fill(false);8 const stack: number[] = [start]; // the "frames" now live on the heap9 while (stack.length > 0) {10 const u = stack.pop()!;11 if (seen[u]) continue;12 seen[u] = true;13 for (const v of g[u]) if (!seen[v]) stack.push(v);14 }15 return seen;16}Key points
- A stack overflow is the stack pointer crossing into a guard page; the fault is detected by the MMU, not by a counter — except in runtimes that add their own check.
- Limits: ~8 MB main thread on Linux/macOS, 1 MB on Windows; secondary threads are smaller (512 kB on macOS) and fixed-size.
- C/C++ report it as
SIGSEGV(exit 139); V8 throws a catchableRangeError; CPython raisesRecursionErrorat a soft limit of 1,000 frames. - Large frames can jump over the guard page; stack probes and the 1 MB stack gap exist to stop that.
- Recursion depth is a correctness bound, not a style question: tree height, path length, and adversarial inputs decide it.
- Any recursion converts to a loop with an explicit heap-allocated stack; do it when the depth depends on untrusted input.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why is the stack limited at all when there is gigabytes of address space?
Every thread needs its own stack, reserved up front. 1,000 threads × 8 MB is 8 GB of address space; limits keep that affordable, and a runaway recursion is better stopped at 8 MB than at 8 GB.
▸Why a guard page instead of a check on every call?
A check costs instructions on every call; the guard page costs nothing until the overflow happens, because the MMU already checks every access. Managed runtimes add a check anyway because they want an exception, not a signal.
▸Why does Python stop at 1,000 when C++ allows 170,000?
CPython frames are heavy heap objects and, before 3.11, each Python call also consumed C stack in the evaluator. A conservative soft limit gives a clean exception instead of a segfault in the interpreter.
▸Why does the crash only happen on big inputs?
Because depth is a function of the input, not of the code. A recursive tree walk is fine on a balanced tree and overflows on a degenerate one; DFS is fine on a grid and overflows on a long path.
Stack overflow
void recurse(int n) {
char buf[224]; // 256 B frame incl. return address + saved rbp
recurse(n + 1);
}How it fails
What the failure looks like from inside real software.
- A recursive JSON or expression parser on deeply nested untrusted input:
RangeError/RecursionErrorin the best case, a denial-of-service crash in C++. - A worker-thread pool running recursive user code overflows at 512 kB on macOS while the same code passes on Linux’s 8 MB main thread.
- Raising
sys.setrecursionlimitto a huge value to "fix" a RecursionError converts a clean exception into a hard segfault of the interpreter. - A 2 MB local buffer skips the guard page and silently corrupts the mapping below the stack — a heisenbug that stack-clash protection would have turned into a crash.
- Mutual recursion through a signal handler or a destructor chain (
~Nodedeleting the next node of a million-element list) overflows on shutdown rather than in normal use.