Understanding Segmentation Faults
A segmentation fault (often shortened to segfault) is a specific kind of memory access violation that occurs when a C or C++ program attempts to read from or write to a memory address that it is not allowed to access. The operating system's memory protection mechanism detects this illegal access and sends the SIGSEGV signal to the process, which by default causes the program to terminate abruptly with the infamous message: Segmentation fault (core dumped).
At the hardware level, the CPU's Memory Management Unit (MMU) works with the operating system to define valid memory regions for each process. When your code tries to touch an address outside these regions—whether it's address zero, a freed chunk of heap memory, or a random pointer value—the MMU triggers a page fault that the OS cannot resolve, resulting in the segmentation fault signal.
Why Understanding Segfaults Is Critical
Segmentation faults are not just annoying crashes. They are symptoms of deeper bugs in your code that can lead to:
- Unpredictable program behavior — crashes may appear intermittent, depending on memory layout and runtime conditions
- Security vulnerabilities — many exploits begin with memory corruption bugs that, if left unfixed, can be leveraged for arbitrary code execution
- Data corruption — before the segfault occurs, your program may have already silently corrupted heap metadata or overwritten critical data structures
- Difficult debugging in production — segfaults in deployed software can cause service downtime and are notoriously hard to reproduce without proper diagnostic infrastructure
Every C/C++ developer must master segfault diagnosis. It's not a question of if you'll encounter one, but when. This guide gives you a systematic, repeatable process to find and fix segmentation faults quickly.
Common Causes of Segmentation Faults
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before diving into tools, let's catalog the root causes. Recognizing patterns speeds up diagnosis dramatically.
1. Dereferencing a NULL Pointer
The most classic cause. A pointer initialized to NULL or nullptr points to address zero, which is never mapped in user space.
// Example: NULL pointer dereference
int *ptr = NULL;
*ptr = 42; // SEGFAULT — writing to address 0x0
2. Dereferencing a Dangling Pointer (Use-After-Free)
After free() or delete, the memory may be returned to the OS or reused. Accessing it afterward is undefined behavior and often causes a segfault.
// Example: use-after-free
int *ptr = new int(100);
delete ptr;
*ptr = 200; // SEGFAULT — ptr now points to freed memory
3. Stack Overflow
Exceeding the stack limit—commonly 8 MB on Linux—by allocating huge local variables or through infinite (or very deep) recursion causes a segfault when the stack pointer hits a guard page.
// Example: infinite recursion causing stack overflow
void recurse() {
recurse(); // Each call consumes stack; eventually segfaults
}
// Example: huge stack allocation
void bad_function() {
char buffer[100000000]; // ~100 MB, exceeds typical stack limit
buffer[0] = 'A'; // SEGFAULT on many systems
}
4. Buffer Overflow / Out-of-Bounds Access
Writing past the end of an array corrupts adjacent memory. This may or may not cause an immediate segfault, depending on whether the corrupted address falls in a valid page. However, if the overflow reaches an unmapped region, a segfault occurs.
// Example: buffer overflow on the heap
int *arr = new int[10];
for (int i = 0; i <= 1000; i++) {
arr[i] = i; // Eventually segfaults when i goes far enough out of bounds
}
5. Reading from Uninitialized / Wild Pointers
A pointer that was never initialized contains garbage data (whatever bits happened to be on the stack). Dereferencing it is a lottery—sometimes it lands in a valid address, sometimes it segfaults.
// Example: uninitialized pointer
int *p; // p contains random stack garbage
*p = 42; // Undefined behavior; often segfaults
6. Double Free or Invalid Free
Freeing memory that was already freed, or freeing a pointer that was not obtained from malloc/new, corrupts the heap allocator's internal data structures. Subsequent allocation or deallocation calls may segfault.
// Example: double free
int *p = new int(5);
delete p;
delete p; // Undefined behavior; may segfault immediately or later
7. Modifying Read-Only Memory (String Literals)
String literals in C/C++ are typically stored in a read-only data segment. Attempting to modify them causes a segfault.
// Example: modifying a string literal
char *s = "Hello, world!"; // s points to read-only memory
s[0] = 'h'; // SEGFAULT — writing to .rodata section
8. Misaligned Memory Access (Architecture-Specific)
Some architectures (like SPARC or older ARM) require aligned access. On x86/x86-64 this is usually tolerated (with a performance penalty), but it can still cause issues in certain contexts, especially with SIMD instructions.
9. Corrupted Heap Metadata
Writing past the boundaries of a heap-allocated buffer corrupts the allocator's internal bookkeeping structures (e.g., free lists, chunk headers). The crash often occurs later, during a seemingly innocent malloc or free call, making diagnosis tricky.
Systematic Diagnosis Workflow
When you encounter a segmentation fault, follow this step-by-step workflow. Do not skip steps—methodical diagnosis beats guesswork every time.
Step 1: Enable Core Dumps
A core dump is a snapshot of the process's memory at the moment of the crash. It's your single most valuable artifact for post-mortem analysis. On Linux, ensure core dumps are enabled:
# Check current core dump settings
ulimit -c
# Enable unlimited core dump size (for current shell and child processes)
ulimit -c unlimited
# Make it permanent by adding to /etc/security/limits.conf:
# * soft core unlimited
# On many modern systems, core dumps are handled by systemd and stored in
# /var/lib/systemd/coredump/ — check with:
coredumpctl list
Step 2: Reproduce the Fault Consistently
Intermittent segfaults are the hardest. Try to find a reliable reproduction scenario:
- Use the same input data each time
- Set environment variables that affect memory layout (
MALLOC_PERTURB_on glibc systems) - Consider using
rr(Mozilla's record-and-replay debugger) to capture a recording of one failure and replay it deterministically
Step 3: Get a Stack Trace with GDB
GDB (GNU Debugger) is the primary tool for analyzing segfaults. Attach it to your core dump or run your program directly under GDB.
# Compile with debugging symbols (-g) and without optimization if possible (-O0)
g++ -g -O0 -o myprogram myprogram.cpp
# Run under GDB directly
gdb ./myprogram
# Within GDB: type 'run' to start, program will stop at segfault
# Alternatively, analyze a core dump
gdb ./myprogram core
# Or with systemd:
gdb ./myprogram (coredumpctl info PID | grep 'Storage' and extract the core file)
Once GDB stops at the segfault, use these commands:
# Print the stack backtrace — shows exactly which function chain led to the crash
(gdb) bt
(gdb) bt full # includes local variable values
# Inspect the current frame
(gdb) frame 0
(gdb) info registers # shows register values, including the faulting address in rip/rsp
(gdb) list # shows source code around the crash point
# Examine variables
(gdb) print variable_name
(gdb) print *pointer_name # dereference a pointer safely in GDB
(gdb) x/20x address # examine 20 hex words starting at address
# Check if a pointer is valid
(gdb) print ptr
(gdb) x/1x ptr # if GDB says "Cannot access memory", ptr is bad
Step 4: Identify the Faulting Instruction and Address
In GDB, the signal stop will tell you the exact instruction and the address that caused the fault:
(gdb) info signals
# Look for "Program received signal SIGSEGV, Segmentation fault."
# GDB prints something like:
# 0x00007f123456789a in __strcpy_avx2 () from /lib64/libc.so.6
# or:
# 0x0000000000401234 in my_function (ptr=0x0) at myprogram.cpp:42
# The address after 'in' is the faulting instruction pointer (rip)
# The faulting data address is often in another register (like rax, rdx)
(gdb) info registers rax rdx rsi rdi
If the faulting address is 0x0, it's a NULL pointer dereference. If it looks like 0x19 or 0x20, it may be a small offset into a struct via a NULL pointer. If it looks like a heap address that seems "off" (e.g., 0x21c5018), suspect use-after-free or heap corruption.
Step 5: Use Valgrind for Heap and Use-After-Free Errors
Valgrind's Memcheck tool tracks every memory allocation, deallocation, and access. It can detect problems that don't immediately cause a segfault, like reading uninitialized memory or accessing slightly out-of-bounds.
# Install valgrind: sudo apt install valgrind (Debian/Ubuntu)
# sudo dnf install valgrind (Fedora)
# Run your program under Valgrind with full leak and error checking
valgrind --tool=memcheck --leak-check=full --track-origins=yes \
--show-reachable=yes --error-limit=no ./myprogram
# Key flags explained:
# --leak-check=full : detailed memory leak report
# --track-origins=yes : shows where uninitialized values came from
# --show-reachable=yes : reports even reachable leaks
# --error-limit=no : don't stop after 1000 errors
Valgrind output for a segfault-causing bug will show something like:
==12345== Invalid write of size 4
==12345== at 0x401234: main (badprog.cpp:15)
==12345== Address 0x51fc040 is 0 bytes after a block of size 40 alloc'd
==12345== at 0x4C2BABC: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345== by 0x401220: main (badprog.cpp:12)
This tells you exactly: the write was 4 bytes, it occurred at line 15, and the memory was originally allocated as a 40-byte block at line 12—meaning you wrote past the end of that block.
Step 6: Enable AddressSanitizer (ASan) for Instant Feedback
AddressSanitizer (ASan) is a compiler-based tool (available in GCC and Clang) that instruments your code at compile time to detect memory errors at runtime. It's faster than Valgrind and catches many of the same bugs.
# Compile with AddressSanitizer
g++ -fsanitize=address -fno-omit-frame-pointer -g -O1 -o myprogram myprogram.cpp
# Or with Clang:
clang++ -fsanitize=address -fno-omit-frame-pointer -g -O1 -o myprogram myprogram.cpp
# Run normally — ASan reports errors immediately upon occurrence
./myprogram
# Additional useful sanitizers:
# -fsanitize=undefined : catches undefined behavior (signed overflow, etc.)
# -fsanitize=leak : lightweight leak detector
# -fsanitize=memory : catches use of uninitialized memory (Clang only)
ASan output is extremely precise. For a heap buffer overflow, you'll see something like:
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000f014
WRITE of size 4 at 0x60300000f014 thread T0
#0 0x401234 in main badprog.cpp:15
0x60300000f014 is located 0 bytes to the right of 40-byte region [0x60300000efe0,0x60300000f008)
allocated by thread T0 here:
#0 0x7f123456789a in operator new[](unsigned long) (/lib64/libasan.so.5+0x123abc)
#1 0x401220 in main badprog.cpp:12
Step 7: Use Static Analysis for Prevention
Catch bugs before they become segfaults. Static analyzers scan your source code without running it:
# Clang Static Analyzer
clang --analyze myprogram.cpp
# Cppcheck (lightweight, fast)
cppcheck --enable=all --inconclusive myprogram.cpp
# For larger projects: Coverity, SonarQube, or PVS-Studio
Step 8: Trace System Calls with strace (Linux) / truss (Solaris)
Sometimes a segfault occurs deep in a system call. strace shows every syscall and its result, which can reveal if a segfault happens after a failed mmap or brk.
strace -f -o strace.log ./myprogram
# The log shows the exact syscall sequence leading to the crash
# Look for SIGSEGV in the output
Practical Code Examples: Buggy Code and Fixes
Example 1: Classic NULL Pointer Dereference
Buggy code:
#include <iostream>
struct Node {
int data;
Node* next;
};
void print_data(Node* node) {
// BUG: No NULL check before dereference
std::cout << node->data << std::endl;
}
int main() {
Node* head = nullptr;
print_data(head); // SEGFAULT
return 0;
}
Fixed code:
#include <iostream>
struct Node {
int data;
Node* next;
};
void print_data(Node* node) {
if (node == nullptr) {
std::cout << "Node is null" << std::endl;
return;
}
std::cout << node->data << std::endl;
}
int main() {
Node* head = nullptr;
print_data(head); // Safe: prints "Node is null"
return 0;
}
Example 2: Use-After-Free (Dangling Pointer)
Buggy code:
#include <iostream>
#include <cstring>
struct Buffer {
char* data;
size_t size;
};
Buffer create_buffer(const char* src) {
Buffer buf;
buf.size = strlen(src) + 1;
buf.data = new char[buf.size];
strcpy(buf.data, src);
return buf; // Returns struct with valid pointer
}
void use_buffer() {
Buffer b = create_buffer("hello");
delete[] b.data; // Free the memory
// ... many lines of code later ...
std::cout << b.data << std::endl; // SEGFAULT: use-after-free
}
int main() {
use_buffer();
return 0;
}
Fixed code:
#include <iostream>
#include <cstring>
#include <memory>
struct Buffer {
std::unique_ptr<char[]> data; // Smart pointer manages lifetime
size_t size = 0;
};
Buffer create_buffer(const char* src) {
Buffer buf;
buf.size = strlen(src) + 1;
buf.data = std::make_unique<char[]>(buf.size);
strcpy(buf.data.get(), src);
return buf;
}
void use_buffer() {
Buffer b = create_buffer("hello");
std::cout << b.data.get() << std::endl; // Safe: data still valid
// No manual delete needed — unique_ptr handles cleanup automatically
}
int main() {
use_buffer();
return 0;
}
Example 3: Stack Overflow from Deep Recursion
Buggy code:
// Recursive Fibonacci without optimization
long long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2); // Exponential recursion depth
}
int main() {
long long result = fibonacci(100000); // Stack overflow segfault
return 0;
}
Fixed code — iterative approach:
// Iterative Fibonacci with O(n) time, O(1) stack space
long long fibonacci(int n) {
if (n <= 1) return n;
long long prev = 0, curr = 1;
for (int i = 2; i <= n; i++) {
long long next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
int main() {
long long result = fibonacci(100000); // Safe: no recursion, no stack overflow
return 0;
}
Alternative fixed code — increase stack size (Linux):
// For cases where recursion is genuinely needed, increase stack limit
// Compile and then set stack size before running:
// ulimit -s unlimited or ulimit -s 65536 (64 MB stack)
// Or use pthreads to set a custom stack size for a specific thread:
#include <pthread.h>
#include <cstdlib>
void* thread_func(void* arg) {
// Deep recursion happens here on a thread with larger stack
return nullptr;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
// Set stack size to 16 MB (16 * 1024 * 1024 bytes)
pthread_attr_setstacksize(&attr, 16 * 1024 * 1024);
pthread_create(&thread, &attr, thread_func, nullptr);
pthread_attr_destroy(&attr);
pthread_join(thread, nullptr);
return 0;
}
Example 4: Buffer Overflow on the Heap
Buggy code:
#include <cstring>
#include <iostream>
void copy_string(const char* src) {
// BUG: Fixed-size buffer with no bounds checking
char* buffer = new char[16];
strcpy(buffer, src); // If src is > 15 chars, buffer overflows
std::cout << buffer << std::endl;
delete[] buffer;
}
int main() {
copy_string("This string is way too long for a 16-byte buffer!"); // SEGFAULT or heap corruption
return 0;
}
Fixed code:
#include <cstring>
#include <iostream>
void copy_string(const char* src) {
// FIX: Allocate exactly what we need
size_t len = strlen(src);
char* buffer = new char[len + 1]; // +1 for null terminator
strcpy(buffer, src); // Safe: buffer is exactly the right size
std::cout << buffer << std::endl;
delete[] buffer;
}
int main() {
copy_string("This string is way too long for a 16-byte buffer!"); // Works perfectly
return 0;
}
Even safer — using standard library containers:
#include <string>
#include <iostream>
void copy_string(const std::string& src) {
std::string buffer = src; // std::string manages memory automatically
std::cout << buffer << std::endl;
// No manual memory management at all
}
int main() {
copy_string("Any length string works, no buffer overflow possible!");
return 0;
}
Example 5: Modifying a String Literal
Buggy code:
#include <iostream>
void to_uppercase(char* str) {
while (*str) {
if (*str >= 'a' && *str <= 'z') {
*str = *str - 'a' + 'A'; // Modifies the string in-place
}
str++;
}
}
int main() {
char* greeting = "hello"; // Points to read-only string literal
to_uppercase(greeting); // SEGFAULT: writing to read-only memory
std::cout << greeting << std::endl;
return 0;
}
Fixed code:
#include <iostream>
#include <cstring>
void to_uppercase(char* str) {
while (*str) {
if (*str >= 'a' && *str <= 'z') {
*str = *str - 'a' + 'A';
}
str++;
}
}
int main() {
// FIX: Use a mutable array, initialized from the literal
char greeting[] = "hello"; // Array on stack, writable
to_uppercase(greeting); // Safe: modifies stack-allocated copy
std::cout << greeting << std::endl; // Prints "HELLO"
return 0;
}
Example 6: Iterator Invalidation (C++ STL)
Buggy code:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.push_back(6); // May invalidate iterators (reallocation)
}
std::cout << *it << " "; // SEGFAULT if reallocation occurred
}
return 0;
}
Fixed code:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// FIX: Reserve enough capacity upfront to avoid reallocation
vec.reserve(100);
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.push_back(6); // Safe: no reallocation since we reserved space
}
std::cout << *it << " ";
}
return 0;
}
Advanced Diagnostic Techniques
Using Watchpoints in GDB
When you suspect a variable is being overwritten but don't know where, set a hardware watchpoint. GDB will break exactly when the memory location is modified.
(gdb) break main
(gdb) run
(gdb) watch my_variable # Break when my_variable changes
(gdb) watch *0x60012345 # Break when memory at this address changes
(gdb) continue # Program runs until the watched value is modified
# When it breaks, bt shows exactly who modified it
Using MALLOC_CHECK_ and MALLOC_PERTURB_ Environment Variables (glibc)
The GNU C library (glibc) provides runtime environment variables that help expose heap bugs earlier:
# MALLOC_CHECK_ sets the heap checking level
# Level 0: no checking (default)
# Level 1: print error messages to stderr on detected corruption
# Level 2: abort() on detected corruption (generates core dump)
export MALLOC_CHECK_=2
# MALLOC_PERTURB_ fills allocated/freed memory with a pattern
# This makes use-after-free and uninitialized reads more likely to crash
export MALLOC_PERTURB_=85 # Fill with byte pattern 0x55 (85 decimal)
# Run your program with both set:
MALLOC_CHECK_=2 MALLOC_PERTURB_=85 ./myprogram
Using Electric Fence for Immediate Segfault on Buffer Overflow
Electric Fence (libefence) places a guard page (no-access) after each allocation. Any out-of-bounds access immediately segfaults instead of silently corrupting memory.
# Install: sudo apt install electric-fence
# Link with libefence:
g++ -g -o myprogram myprogram.cpp -lefence
# Or preload it:
LD_PRELOAD=/usr/lib/libefence.so ./myprogram
# Warning: Electric Fence uses enormous amounts of virtual memory.
# It's best for debugging small, isolated test cases.
Debugging Shared Library Load Issues
Sometimes segfaults occur before main() due to shared library loading failures. Use ldd and LD_DEBUG:
# Check library dependencies
ldd ./myprogram
# Verbose loader diagnostics
LD_DEBUG=all ./myprogram 2>&1 | grep -i error
# Check for ABI mismatches with nm and objdump
nm -D ./myprogram | grep UNDEFINED
objdump -T ./myprogram | grep DF_1
Best Practices to Prevent Segmentation Faults
1. Initialize All Pointers Immediately
Never declare a pointer without initialization. Set it to nullptr, a valid address, or use a smart pointer.
// Bad
int *p;
// Good
int *p = nullptr;
// Better
std::unique_ptr<int> p = std::make_unique<int>(42);
2. Use RAII and Smart Pointers in C++
Modern C++ provides std::unique_ptr, std::shared_ptr, and std::weak_ptr. These automate memory management and prevent use-after-free and double-free errors entirely.
// Instead of raw new/delete:
std::unique_ptr<MyClass> obj = std::make_unique<MyClass>(args);
// obj automatically destroyed when it goes out of scope — no manual delete needed
3. Use Standard Containers Instead of Raw Arrays
Prefer std::vector, std::array, std::string over raw C arrays and char*. These containers manage memory and provide bounds-checked access via .at().
// Bad: raw array with potential overflow
char buf[256];
sprintf(buf, "%s", user_input);
// Good: std::string handles allocation and resizing automatically
std::string buf = user_input;
4. Enable Compiler Warnings and Treat Them as Errors
Modern compilers can statically detect many segfault-prone patterns.
# GCC: enable all warnings
g++ -Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -o myprogram myprogram.cpp
# Clang: even more diagnostics
clang++ -Weverything -Werror -o myprogram myprogram.cpp
5. Use Bounds-Checked Access Methods
When using containers, .at() throws an exception on out-of-bounds access instead of causing undefined behavior (and potential segfaults).
std::vector<int> vec = {1, 2, 3};
// vec[100] = 42; // Undefined behavior, might segfault
vec.at(100) = 42; // Throws std::out_of_range exception
6. Test with Sanitizers in CI/CD Pipelines
Make AddressSanitizer, UndefinedBehaviorSanitizer, and MemorySanitizer part of your continuous integration testing. Catch memory bugs before they reach production.
# Example CI job: build and run tests with ASan + UBSan
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-o test_program test_program.cpp
./test_program
7. Avoid Recursion for Unbounded Depth Problems
If the recursion depth depends on user input or runtime data, prefer iterative algorithms or explicit stack data structures on the heap.
8. Check Return Values and Validate Input
Many segfaults originate from functions returning NULL on failure (malloc, fopen, etc.). Always check.
// Bad
FILE* f = fopen("config.txt", "r");
fscanf(f, "%s", buffer); // SEGFAULT if fopen failed
// Good
FILE* f = fopen("config.txt", "r");
if (f == nullptr) {
perror("Failed to open config.txt");
return 1;
}
fscanf(f, "%s", buffer);
fclose(f);
9. Use Static Code Analysis in Your Workflow
Integrate tools like clang-tidy, cppcheck, or Coverity into your build process. They catch many potential segfault causes at compile time.
# clang-tidy example
clang-tidy myprogram.cpp --checks=* --warnings-as-errors=*
10. Document Pointer Ownership Explicitly
In team projects, use clear conventions: document which function or object owns a pointer and is responsible for freeing it. Even better, encode ownership in the type system using std::unique_ptr (exclusive ownership) or std::shared_ptr (shared ownership).
Debugging Multi-Threaded Segfaults
Segfaults in multi-threaded programs add complexity because the faulting thread may not be the one that caused the corruption. Memory corruption from a data race in one thread can cause a segfault much later in another thread.
Use ThreadSanitizer (TSan)
# Compile with ThreadSanitizer
g++ -fsanitize=thread -g -O1 -o myprogram myprogram.cpp -lpthread
# Run — TSan detects data races in real time
./myprogram
GDB Multi-Threaded Debugging Commands
(gdb) info threads # List all threads
(gdb) thread apply all bt # Backtrace for every thread
(gdb) thread 2 # Switch to thread #2
(gdb) set scheduler-locking on # Only current thread runs during step/next
Helgrind (Valgrind Tool for Thread Errors)
valgrind --tool=helgrind --free-is-write=yes ./myprogram
# Helgrind detects: data races, inconsistent lock ordering, deadlocks