What Is a Segmentation Fault? The Crash That Tells You Nothing

- Segmentation fault (SIGSEGV) fires when a process touches memory outside its allowed regions; the OS kills it immediately with no recovery
- The four root causes: null pointer dereference, buffer overflow, stack overflow via infinite recursion, and use-after-free
- Address zero is deliberately unmapped so null dereferences fail loudly instead of silently corrupting adjacent memory
- Managed languages (Python, Java, JS) replace segfaults with catchable exceptions, but C extensions and JNI calls bypass those protections entirely
- AddressSanitizer (
-fsanitize=address) is the fastest path from "segfault somewhere" to the exact line and cause - A segfault is the better outcome — silent same-page corruption is far harder to trace than an immediate process kill
Your program ran. Then it didn't. The terminal printed two words and quit: Segmentation fault. No stack trace. No line number. No apology. Just a dead process and that specific variety of rage that only truly useless error messages can produce.
A segmentation fault happens when your program tries to access memory it isn't allowed to touch. The operating system enforces hard boundaries around your program's memory, and when your code crosses one, the OS kills the process immediately. No recovery, no cleanup, no explanation beyond the signal number. The OS isn't being mean. It just doesn't owe you a longer message.
Your Program's Memory Is Not One Big Open Field
When a process starts, the OS divides its virtual address space into distinct regions. The code segment holds your compiled instructions, read-only and executable. The data segment holds global and static variables. The stack holds function call frames and local variables. The heap holds dynamic allocations. And then there's everything else: unmapped virtual addresses that don't correspond to any physical memory at all. No-man's land.
Each region has permissions enforced by hardware. The CPU's Memory Management Unit (MMU) checks every memory access against a table of allowed regions and their permissions. When your program reads from an address on the stack, the MMU verifies that read is permitted. This check happens at the hardware level, on every access, with no overhead visible to your code.
When an access violates those permissions, the MMU raises a hardware exception, the OS kernel catches it, and sends the SIGSEGV signal to the offending process. SIGSEGV is signal number 11 on most Unix systems. Its name comes from memory segmentation, an older CPU model for dividing memory into protected regions. Modern systems use paging instead, but the terminology survived. So now you're staring at two words while the MMU calmly files an incident report and goes back to its day.
The message "Segmentation fault (core dumped)" means the OS also wrote a snapshot of process memory to disk. That snapshot is the raw material for post-mortem debugging with gdb. The core dump is the black box recorder for your crashed program. You're welcome.
The Four Bugs Behind Most Segfaults
Null Pointer Dereference
Address zero is never mapped to anything in a normal process. The OS deliberately leaves the first few pages unmapped so that null pointer dereferences fail loudly rather than silently corrupting data elsewhere. This is one of the few times "fail loudly" is a feature.
int *ptr = NULL; int value = *ptr; // SIGSEGV
In interviews this shows up most often during linked list traversal. You walk to the last node, then try to access current->next->data without checking whether current->next is null.
Node *current = head; while (current->next != NULL) { current = current->next; } // Safe here. But what if head itself was NULL? // The first iteration already dereferenced it.
Always check for null before the dereference, not after. The check belongs at the entry point of the traversal, before anything touches the pointer. Not halfway through. Not on the next line. Before.
Buffer Overflow
Arrays in C are just pointers to the first element with no bounds information attached. Writing past the end writes into whatever memory follows the allocation. That might be other local variables, control metadata, or a completely unmapped page.
int arr[5]; for (int i = 0; i <= 5; i++) { // <= instead of < arr[i] = i; // arr[5] is past the end }
If the out-of-bounds write lands on a mapped page, you get silent corruption. If it lands on an unmapped page, you get a segfault. The segfault is the better outcome. At least you find out immediately and not six hours later in production when a user triggers the exact input you never tested.
Off-by-one errors in loop bounds are the most common source of buffer overflows in coding interview problems. The wrong answer or crash you see at the test case boundary often originates here. It's always <= when it should be <. Every. Single. Time.

The entire buffer overflow bug taxonomy, in one bar joke.
Stack Overflow
The stack has a fixed size. Linux and macOS default to 8 MB. Windows defaults to 1 MB. Every function call pushes a new frame containing local variables, the return address, and saved registers. Infinite recursion eats through this budget frame by frame, quiet and efficient, like a very patient memory leak.
int factorial(int n) { return n * factorial(n - 1); // No base case }
When the stack is full, the next call attempts to push a frame into unmapped memory just below the stack region. The MMU fires. SIGSEGV arrives. The process terminates. See what causes a stack overflow for the detailed mechanics of how the stack region interacts with guard pages.
Use-After-Free
When you free() a heap allocation and then access it anyway, you're reading or writing memory that may have been reallocated to a completely different part of the program.
int *ptr = malloc(sizeof(int)); *ptr = 42; free(ptr); int value = *ptr; // Undefined behavior
Use-after-free is dangerous precisely because it doesn't always crash immediately. The freed memory often still contains your old value, so the program appears to work. The crash or corruption surfaces later, in a completely unrelated location, once the allocator reuses that memory for something else. By then you have no idea where to look. The bug has become someone else's problem, and that someone else is future you.
What Managed Languages Do Instead
Python, Java, and JavaScript don't give you raw pointers. Their runtimes intercept every memory access through their own safety layers. Think of it as bubble wrap for your code.
In Python, accessing a list with an out-of-bounds index raises IndexError. Calling a method on None raises AttributeError. Java throws ArrayIndexOutOfBoundsException and NullPointerException. JavaScript throws TypeError. These are catchable exceptions that terminate the current operation, not signals that kill the entire process.

The managed language safety net: still embarrassing, but at least your process survives.
This protection has limits. CPython itself is written in C. If you use ctypes to call a C function and pass an argument with the wrong type or size, the C code can dereference an invalid address and kill the Python interpreter with SIGSEGV. The same outcome is possible if you push Python's recursion ceiling far above the actual system stack size via sys.setrecursionlimit(). Once CPython blows the OS stack, the fault bypasses Python's own RecursionError handling entirely.
Managed languages protect you from bugs in your Python or Java code. They don't protect you from bugs in C extensions, JNI calls, or runtime configuration that violates OS limits.
C/C++ Patterns That Kill Interview Code
If you're writing C++ in a systems interview, these patterns come up directly.
Returning a pointer to a local variable. The variable lives on the stack frame of the function that declared it. When the function returns, that frame is gone. The pointer still holds the address, but the memory now belongs to whatever function runs next.
int* broken() { int x = 42; return &x; // x is destroyed when broken() returns }
Using an uninitialized pointer. An uninitialized pointer holds whatever garbage was in that memory location. Dereferencing it writes to or reads from a random address.
int *ptr; // Uninitialized: points to a garbage address *ptr = 10; // Writes to that random address
Double free. Calling free() twice on the same pointer corrupts the heap's internal bookkeeping structures, which produces undefined behavior on the next allocation.
The pointer vs reference distinction in C++ eliminates two of these failure modes at the language level. References can't be null and can't be reseated. That's not a small thing.
How to Debug a Segmentation Fault
Three tools cover most cases. Pick the one that matches how much time you have.
gdb is the standard debugger for C/C++ crashes. Run the program inside gdb, and after the crash, bt (backtrace) prints the exact call stack with file names and line numbers. If you have a core dump, gdb ./program core loads the crash snapshot directly. This is the "I have time" option.
AddressSanitizer (ASan) instruments every memory access at build time. Compile with -fsanitize=address on clang or gcc, run the program, and ASan prints the exact invalid access with a full stack trace and the type of error. ASan is the fastest path from "segfault somewhere" to "exact line and cause", and it's what most modern C++ CI pipelines run. This is the "I want answers now" option.
Valgrind runs your program in a memory-tracking emulator and catches invalid accesses, use-after-free, and uninitialized reads. It slows execution by ten to fifty times, but catches errors that manifest as subtle corruption rather than immediate crashes. This is the "I suspect something deeper is wrong" option.
For Python, import faulthandler; faulthandler.enable() at the top of a script gives you a minimal thread backtrace even when a C extension crashes the interpreter.
Segmentation Faults Are the Good Crash
A segfault is the OS enforcing the only protection mechanism it has: killing the process that violated the rules. That sounds brutal. It is actually the good outcome.
The bugs that don't cause segfaults, the ones that corrupt adjacent memory silently, are far more dangerous. An off-by-one that writes one element past the end of an array on a mapped page will run without error. The corruption propagates, the program produces wrong results or crashes much later in a completely different location, and the original cause is nearly impossible to trace. The segfault was trying to protect you.
That's the core motivation behind memory-safe languages like Rust. The borrow checker statically proves at compile time that no reference outlives its data and no array access can exceed its bounds without an explicit check. Segfaults aren't caught at runtime. They're made impossible at compile time. The compiler is doing the MMU's job before the code ever runs.
In coding interviews, treating every pointer as potentially null and every array access as potentially out of bounds catches most segfault-producing bugs before you submit. If you want to practice catching these under realistic pressure, SpaceComplexity runs voice-based mock interviews where you explain your null checks and loop invariants out loud. Narrating your reasoning catches errors that silent typing misses.