Understanding Segmentation Faults
What Is a Segmentation Fault?
A segmentation fault (often shortened to segfault) is a signal delivered to a process by the operating system when it attempts to access a memory location that it is not allowed to access. In C and C++, this is signal SIGSEGV (signal number 11 on most Unix-like systems). The kernel sends this signal when a program tries to read from or write to an invalid memory address, such as:
- Dereferencing a null pointer
- Accessing memory beyond the bounds of an allocated block or array
- Accessing memory that has already been freed (dangling pointer)
- Accessing memory without proper permissions (e.g., writing to read-only segments)
- Stack overflow from excessive recursion or large local variables
When a segmentation fault occurs, the default behavior is for the process to terminate immediately, often with the cryptic message Segmentation fault (core dumped) printed to the terminal. This crash happens because the CPU's memory management unit (MMU) detects the illegal access and raises an exception that the OS translates into SIGSEGV.
Why It Matters
Segmentation faults are among the most common and frustrating bugs in C and C++ development. They matter because:
- They crash your program — there is no graceful recovery by default, leading to data loss or poor user experience.
- They are symptoms of deeper memory corruption — a segfault often indicates that your program has undefined behavior lurking elsewhere, which may have already silently corrupted data before the crash.
- They can be security vulnerabilities — many exploits (buffer overflows, use-after-free) start by triggering a controlled memory access violation that an attacker can leverage.
- They are notoriously hard to reproduce — segfaults may appear intermittently depending on memory layout, allocation patterns, and system load, making them a debugging nightmare.
Understanding how to systematically diagnose and fix segmentation faults is an essential skill for any C or C++ programmer. The good news is that with modern tools and disciplined practices, you can track down even the most elusive segfaults and prevent them from recurring.
Common Causes of Segmentation Faults
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →1. Null Pointer Dereference
This is the most straightforward cause. A pointer that is NULL (or nullptr in C++) does not point to any valid memory. Dereferencing it triggers an immediate segfault because address 0x0 is typically mapped as inaccessible by the OS.
int *ptr = NULL;
*ptr = 42; // SIGSEGV: writing to address 0
2. Array Index Out of Bounds
Accessing an array element beyond its allocated size reads or writes into memory that does not belong to the array. If that memory happens to be unmapped or protected, a segfault occurs.
int arr[10];
arr[10] = 99; // valid indices are 0..9; index 10 is out of bounds
// This may or may not segfault immediately depending on what lies beyond arr[9]
3. Buffer Overflow / Overrun
Similar to array out-of-bounds but typically refers to writing past the end of a dynamically allocated buffer or filling a fixed-size buffer with more data than it can hold (e.g., via strcpy without bounds checking).
char buffer[8];
strcpy(buffer, "This string is way too long!"); // overflow, may segfault
4. Use-After-Free (Dangling Pointer)
After free() or delete is called on a pointer, the memory it pointed to is returned to the heap allocator and may be reused. Accessing that memory afterward is undefined behavior and often results in a segfault.
int *p = (int*)malloc(sizeof(int));
free(p);
*p = 10; // use-after-free: p is now a dangling pointer
5. Double Free
Calling free() or delete twice on the same pointer corrupts the heap's internal metadata and can cause a segfault either during the second free or during a subsequent allocation.
int *p = (int*)malloc(sizeof(int));
free(p);
free(p); // double free: heap corruption, likely segfault
6. Stack Overflow
Deep or infinite recursion, or allocating very large local variables on the stack, can exhaust the stack space. Writing beyond the stack boundary triggers a segfault.
void recurse() {
recurse(); // infinite recursion, eventually overflows the stack
}
// Alternative: char huge[100000000]; on the stack may also segfault
7. Uninitialized / Wild Pointers
A pointer declared but not initialized contains a garbage value. Dereferencing it accesses a random memory address, which almost certainly triggers a segfault or corrupts data silently.
int *p; // uninitialized: contains random address
*p = 42; // writing to a random memory location
8. Incorrect Format Specifiers in printf/scanf
Using the wrong format specifier can cause the variadic functions to interpret arguments incorrectly, potentially dereferencing invalid pointers.
char *str = "hello";
printf("%d", str); // passing a pointer but interpreting it as an integer — undefined behavior
// Even worse: scanf("%s", 12345); passes an integer as a pointer — likely segfault
9. Returning a Pointer to a Local Variable
When a function returns, its stack frame is destroyed. A pointer to a local variable becomes a dangling pointer to freed stack memory.
int *bad_function() {
int local = 42;
return &local; // pointer to stack memory that will be invalid after return
}
// The caller dereferencing this returned pointer may segfault
Debugging Segmentation Faults
Using GDB (GNU Debugger)
GDB is the most powerful tool for diagnosing segfaults interactively. It lets you inspect the program state at the exact moment of the crash, examine the call stack, and print variable values.
Step 1: Compile with debug symbols
gcc -g -O0 -o myprogram myprogram.c # -g includes debug info, -O0 disables optimizations for clarity
Step 2: Run the program inside GDB
gdb ./myprogram
(gdb) run
When the segfault occurs, GDB will print something like:
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401136 in main () at myprogram.c:12
12 *ptr = 42;
Key GDB commands after the crash:
btorbacktrace— show the call stack (which functions were called, in what order)info locals— display local variables in the current frameprint ptr— examine the value of a specific variable or expressionlist— show the source code around the crash pointinfo registers— examine CPU registers to see the exact faulting addressframe N— switch to a different stack frame to inspect callers
Example debugging session:
$ gdb ./myprogram
(gdb) run
Starting program: /home/user/myprogram
Program received signal SIGSEGV, Segmentation fault.
0x00005555555551a9 in compute (data=0x0, n=10) at myprogram.c:25
25 data[0] = 42;
(gdb) bt
#0 compute (data=0x0, n=10) at myprogram.c:25
#1 0x00005555555551d4 in main () at myprogram.c:35
(gdb) print data
$1 = (int *) 0x0
(gdb) frame 1
#1 0x00005555555551d4 in main () at myprogram.c:35
35 result = compute(NULL, 10);
(gdb) list
30 int main() {
31 int *arr = NULL;
32 int result;
33 // forgot to allocate arr
34 result = compute(arr, 10);
35 return 0;
36 }
From this session, you immediately see that arr was never allocated before being passed to compute.
Using Valgrind (Memcheck)
Valgrind's Memcheck tool detects memory errors at runtime without requiring you to attach a debugger interactively. It is excellent for catching use-after-free, invalid reads/writes, memory leaks, and uninitialized value usage.
gcc -g -O0 -o myprogram myprogram.c
valgrind ./myprogram
Valgrind output for a use-after-free example:
==12345== Invalid write of size 4
==12345== at 0x4005A3: main (myprogram.c:10)
==12345== Address 0x4c2f040 is 0 bytes inside a block of size 4 free'd
==12345== at 0x4C2B3F0: free (in /usr/lib/valgrind/...)
==12345== by 0x40059E: main (myprogram.c:9)
==12345== Block was alloc'd at
==12345== at 0x4C2A9F0: malloc (in /usr/lib/valgrind/...)
==12345== by 0x40058D: main (myprogram.c:7)
Valgrind pinpoints the exact line where the invalid write occurred, where the memory was freed, and where it was originally allocated. This is invaluable for tracking down complex heap bugs.
Using Address Sanitizer (ASan)
Address Sanitizer is a compiler-based tool (available in GCC and Clang) that instruments your code at compile time to detect memory errors at runtime with minimal overhead. It catches buffer overflows, use-after-free, double-free, and more, often with very precise error messages.
gcc -fsanitize=address -g -O0 -o myprogram myprogram.c
./myprogram
Example ASan output for a buffer overflow:
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000eff8 at pc 0x0000004008a3
READ of size 4 at 0x60300000eff8 thread T0
#0 0x4008a2 in main myprogram.c:15
0x60300000eff8 is located 0 bytes to the right of 40-byte region [0x60300000efd0,0x60300000eff8)
allocated by thread T0 here:
#0 0x4009e0 in malloc (/lib/...)
#1 0x40087d in main myprogram.c:10
ASan is often faster than Valgrind for day-to-day development and provides excellent stack traces. It is highly recommended to always compile with -fsanitize=address during development and testing.
Analyzing Core Dumps
When a program crashes with "core dumped," it writes a snapshot of its memory and registers to a file (usually named core or core.PID). You can analyze this post-mortem with GDB without re-running the program.
Enable core dumps:
ulimit -c unlimited # allow unlimited core dump file size
Analyze the core file:
gdb ./myprogram core
(gdb) bt
(gdb) info registers
(gdb) print ptr
This is extremely useful for debugging crashes that happen in production or on remote machines where you cannot run a live debugger.
Strategic Print Statements (The "Binary Search" Method)
When tools are unavailable (e.g., on an embedded system), you can narrow down a segfault by inserting print statements to log progress. Use fflush(stdout) after each print to ensure the output is flushed before a crash, so you know exactly which print was the last one displayed.
printf("Checkpoint 1\n"); fflush(stdout);
do_something();
printf("Checkpoint 2\n"); fflush(stdout);
do_something_else();
printf("Checkpoint 3\n"); fflush(stdout); // if this never prints, crash is in do_something_else()
How to Fix Segmentation Faults — Practical Examples
Example 1: Fixing a Null Pointer Dereference
Buggy code:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
void print_node_value(Node *node) {
printf("Value: %d\n", node->value); // BUG: no null check
}
int main() {
Node *head = NULL;
print_node_value(head); // segfault!
return 0;
}
Fixed code with null guard:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
void print_node_value(Node *node) {
if (node == NULL) {
printf("Error: node is NULL, cannot print value.\n");
return;
}
printf("Value: %d\n", node->value);
}
int main() {
Node *head = NULL;
print_node_value(head); // graceful error message instead of crash
return 0;
}
The fix adds a defensive null check before dereferencing the pointer. In production code, you should also validate that head is properly initialized before any operations that assume a non-null list.
Example 2: Fixing an Array Out-of-Bounds Access
Buggy code:
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
// Iterating one past the last element
for (int i = 0; i <= 5; i++) { // BUG: i <= 5 should be i < 5
printf("numbers[%d] = %d\n", i, numbers[i]);
}
return 0;
}
Fixed code:
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int array_size = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < array_size; i++) { // correct: i < array_size
printf("numbers[%d] = %d\n", i, numbers[i]);
}
return 0;
}
The fix uses i < array_size instead of i <= 5. The constant 5 was replaced with a computed array_size to prevent the error from recurring if the array size changes later. Always compute the array size using sizeof(array)/sizeof(array[0]) for static arrays.
Example 3: Fixing a Use-After-Free
Buggy code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *buffer = (char*)malloc(100);
strcpy(buffer, "Hello, world!");
printf("Before free: %s\n", buffer);
free(buffer);
printf("After free: %s\n", buffer); // BUG: using freed memory
return 0;
}
Fixed code with pointer nullification:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *buffer = (char*)malloc(100);
if (buffer == NULL) {
fprintf(stderr, "Memory allocation failed.\n");
return 1;
}
strcpy(buffer, "Hello, world!");
printf("Before free: %s\n", buffer);
free(buffer);
buffer = NULL; // critical: nullify the pointer after freeing
// Any subsequent use of buffer will now be a clean null pointer dereference
// which is easier to detect than a subtle use-after-free
if (buffer != NULL) {
printf("After free: %s\n", buffer);
} else {
printf("Buffer has been freed and is no longer available.\n");
}
return 0;
}
The fix nullifies the pointer immediately after free(). This converts a dangerous use-after-free into a deterministic null pointer dereference that tools like ASan or even a simple null check can catch. It also makes the intent explicit in the code.
Example 4: Fixing a Stack Overflow from Deep Recursion
Buggy code — infinite recursion:
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
// BUG: no base case for n == 0 or n == 1
return n * factorial(n - 1); // infinite recursion for n > 0
}
int main() {
printf("Factorial of 5: %llu\n", factorial(5));
return 0;
}
Fixed code with proper base case:
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
if (n == 0 || n == 1) {
return 1; // base case: stops the recursion
}
return n * factorial(n - 1);
}
int main() {
printf("Factorial of 5: %llu\n", factorial(5));
return 0;
}
Every recursive function must have a base case that does not call itself. For large inputs that could still overflow the stack, consider converting the algorithm to an iterative version:
unsigned long long factorial_iterative(unsigned int n) {
unsigned long long result = 1;
for (unsigned int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Iterative versions use constant stack space and eliminate the risk of stack overflow entirely.
Example 5: Fixing Incorrect scanf Arguments
Buggy code:
#include <stdio.h>
int main() {
char *name; // uninitialized pointer
printf("Enter your name: ");
scanf("%s", name); // BUG: writing to random memory via uninitialized pointer
printf("Hello, %s!\n", name);
return 0;
}
Fixed code with proper buffer and field width limit:
#include <stdio.h>
int main() {
char name[100]; // allocated buffer on the stack
printf("Enter your name: ");
// Use %99s to prevent buffer overflow (leaves room for null terminator)
scanf("%99s", name);
printf("Hello, %s!\n", name);
return 0;
}
The fix replaces the uninitialized pointer with a stack-allocated buffer and uses a width specifier (%99s) to prevent buffer overflow. For even safer input handling, consider using fgets() instead:
char name[100];
printf("Enter your name: ");
if (fgets(name, sizeof(name), stdin) != NULL) {
// Remove trailing newline if present
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0';
}
printf("Hello, %s!\n", name);
}
Best Practices to Prevent Segmentation Faults
1. Always Initialize Pointers
Never declare a pointer without assigning it a valid value. Initialize pointers to NULL if you don't have a valid target yet, or allocate memory immediately.
// Good: initialize to NULL
int *ptr = NULL;
// Later, before use:
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) { /* handle error */ }
// In C++, prefer:
int *ptr = nullptr;
// Or better yet, use smart pointers (see below)
2. Check Return Values of Memory Allocation Functions
malloc, calloc, realloc, and new (in C++ with nothrow) can fail and return NULL. Always check.
int *data = (int*)malloc(1000 * sizeof(int));
if (data == NULL) {
fprintf(stderr, "Memory allocation failed. Exiting.\n");
exit(EXIT_FAILURE);
}
3. Use Bounds Checking and Safe String Functions
Prefer functions that limit the amount of data written:
- Use
strncpy,snprintf,strncatinstead ofstrcpy,sprintf,strcat. - Use
fgetsinstead ofgets(which is removed from modern C standards). - When using
scanf, always specify field widths for strings:scanf("%99s", buffer).
char dest[32];
const char *src = "This is a very long string that will overflow";
snprintf(dest, sizeof(dest), "%s", src); // safe: truncates to fit dest
4. Set Freed Pointers to NULL Immediately
After free(ptr), set ptr = NULL. This turns any accidental subsequent use into a clean crash that is easier to debug than a corrupted heap. In C++, after delete ptr, set ptr = nullptr.
free(ptr);
ptr = NULL; // prevent dangling pointer reuse
5. Use Smart Pointers in C++
C++ offers std::unique_ptr and std::shared_ptr that automatically manage memory and prevent use-after-free and double-free errors.
#include <memory>
#include <iostream>
struct Data {
int value;
Data(int v) : value(v) {}
};
int main() {
// Automatic cleanup when ptr goes out of scope
std::unique_ptr<Data> ptr = std::make_unique<Data>(42);
std::cout << "Value: " << ptr->value << std::endl;
// No manual delete needed — ptr is automatically freed
return 0;
}
6. Enable Compiler Warnings and Treat Them as Errors
Modern compilers can detect many potential segfault causes at compile time.
gcc -Wall -Wextra -Werror -o myprogram myprogram.c
# -Wall: enable all common warnings
# -Wextra: enable extra warnings
# -Werror: treat warnings as errors (forces you to fix them)
7. Use Static Analysis Tools
Tools like cppcheck, clang-tidy, or Coverity can find potential null pointer dereferences, buffer overflows, and other memory bugs without running the program.
cppcheck --enable=all myprogram.c
clang-tidy myprogram.c --checks=* --
8. Compile with Sanitizers During Development and Testing
Always include Address Sanitizer (and optionally Undefined Behavior Sanitizer) in your debug/test builds.
# Development/debug build
gcc -fsanitize=address -fsanitize=undefined -g -O0 -o myprogram_debug myprogram.c
# Release build (without sanitizers for performance)
gcc -O2 -DNDEBUG -o myprogram_release myprogram.c
9. Use Vector and Array Containers in C++
Prefer std::vector, std::array, and std::string over raw C arrays and pointers. These containers manage memory automatically and provide bounds-checked access via at().
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
// at() throws std::out_of_range on invalid index instead of segfaulting
try {
std::cout << numbers.at(10) << std::endl; // throws exception
} catch (const std::out_of_range& e) {
std::cerr << "Index out of bounds: " << e.what() << std::endl;
}
return 0;
}
10. Code Review and Defensive Programming
Write functions that validate their inputs. Use assertions to catch programming errors early in development.
#include <assert.h>
void process_data(int *data, size_t size) {
assert(data != NULL); // catches null pointer during development
assert(size > 0); // catches invalid size
if (data == NULL || size == 0) {
return; // graceful handling in production (when asserts are disabled)
}
// safe processing here
for (size_t i = 0; i < size; i++) {
data[i] *= 2;
}
}
Conclusion
Segmentation faults are a fundamental aspect of systems programming in C and C++. They arise from the direct memory access model that gives these languages their power and performance. Rather than fearing segfaults, developers should treat them as valuable signals that expose latent memory bugs. The key to mastering segfault resolution is a three-part approach: prevention through disciplined coding practices like initializing pointers, bounds checking, and using safe abstractions; detection using modern tools such as GDB, Valgrind, and Address Sanitizer to catch errors early in the development cycle; and systematic debugging that follows the evidence from the crash site back to the root cause. By combining these strategies — and by embracing C++ features like smart pointers, vectors, and RAII when appropriate — you can write robust, crash-free software that fully harnesses the capabilities of native code without suffering from its most notorious pitfalls.