I recently saw “Interview with Senior C++ Developer” by Kai Lentit on Youtube. One of the punch lines in the video was: “My favorite book? ’10 Elegant Ways to Create a SegFault’.” I laughed a bit harder than I probably should have. So here I am, stealing the idea. π
(Although, I’m going to use C instead, because I know much more about the pitfalls in C than I do about C++.)
Before jumping into the list, I should probably explain what segfaults are: For the past 35 years or so, computers have provided an abstraction layer for system memory called virtual memory. The idea is that every process gets it’s own virtual memory space, that the program can’t escape from. This prevents rogue programs from crashing other programs or messing with the operating system. This is done using an MMU – a Memory Management Unit. The operating system provisions some memory for the process, and the MMU translates the virtual regions into real physical memory locations.
Let’s assume a process does some naughty things, like accessing unprovisioned memory. The MMU detects that, and generates a synchronous interrupt, thus informing the operating system. The OS in turn, determines which process caused the interrupt, and signals SIGSEGV
– Segmentation Violation. This will usually kill the process. (Theoretically, you could recover from this, but POSIX actually doesn’t guarantee that.)
Disclaimer
I should probably note that everything I’m doing in this article is bad. I’m crashing my programs deliberately, and almost everything mentioned here is undefined behaviour anyway.
So: Don’t try this at home. (Unless it’s for fun – then try it. π)
Some of the examples only work on certain compilers, certain operating systems, or certain platforms. If not stated otherwise, I’m using clang 14.0.3 on MacOS 14.0 (Darwin 23.0.0) on an Apple Silicon M1 (aarch64; arm8.5-A).
1. NULL Dereference
Uhh, a classic. In programming NULL
(or null, nil, None, …) indicates the absence of data. Or to be more specific, NULL
indicates that a pointer is not valid.
Here is an example for how NULL
can be used in practice: Let’s say we want to allocate some memory on the heap, but the amount we requested is too big (maybe we’ve exhausted the system memory or we ran into a ulimit). The sbrk
/mmap
syscall will fail, and malloc
will return NULL
to indicate that the allocation was not successful (and set errno
to ENOMEM
so the rest of the program knows what went wrong).
NULL
pointers themselves are not that bad. However, we need to make sure not to accidentally dereference them. In C this is actually undefined behaviour. But typically, NULL
points to address 0, which is usually outside the programs virtual memory. Accessing this address will therefore be an access violation.
#include <stdio.h>
int main(void) {
int* i = NULL;
printf("%d\n", *i);
return 0;
}
Code language: C++ (cpp)
$ clang -std=c11 -O0 -Wall -Wpedantic main.c
$ ./a.out
[1] 31790 segmentation fault ./a.out
Other (higher-level) languages that also have a NULL
value, usually check this at runtime (without relying on signals), and raise an exception accordingly. An example is the infamous NullPointerException
in Java.
2. Stack Overflow
No, not the website. π A stack overflow (also called a stack exhaustion) is when a process writes past the memory region that’s reserved for the stack. This could for example happen because of an endless recursion.
#include <alloca.h>
#include <stdio.h>
void recursion(int depth) {
// allocate 1 MiB of memory in each stack frame
// to speed up stack exhaustion
void* buffer = alloca(1 << 20);
fprintf(stderr, "depth: %d: %p\n", depth, buffer);
recursion(depth + 1);
}
int main(void) {
recursion(0);
return 0;
}
Code language: C++ (cpp)
$ clang -std=c11 -O0 -Wall -Wpedantic main.c
main.c:4:27: warning: all paths through this function will call itself [-Winfinite-recursion]
void recursion(int depth) {
^
1 warning generated.
$ ./a.out
depth: 0: 0x16bacf308
depth: 1: 0x16b9cf2b8
depth: 2: 0x16b8cf268
depth: 3: 0x16b7cf218
depth: 4: 0x16b6cf1c8
depth: 5: 0x16b5cf178
depth: 6: 0x16b4cf128
[1] 37255 segmentation fault ./a.out
The result is, of course, undefined. But, it usually trips a segfault on modern operating systems.
3. Stack Buffer Overflow
A stack buffer overflow is kind of the opposite of a stack overflow. It occurs when a program writes past the bounds of a buffer on the stack. At some point the return address of the function will be overwritten, which will cause the program to potentially jump to a non-executable memory location.
#include <alloca.h>
#include <stdio.h>
#include <stdint.h>
__attribute__((no_stack_protector))
void function(void) {
uint64_t* buffer = alloca(sizeof (uint64_t));
buffer[3] = 0;
fprintf(stderr, "return\n");
return;
}
int main(void) {
function();
return 0;
}
Code language: C++ (cpp)
$ clang -std=gnu11 -O0 -Wall -Wpedantic main.c
$ ./a.out
return
[1] 47025 segmentation fault ./a.out
Code language: JavaScript (javascript)
In case you want to have your mind blown, I’d recommend looking up return-oriented programming. This is a hacking technique were you execute arbitrary code by jumping at the end of libc functions. The jump addresses are written to the stack – like in the program above. I even read somewhere that ROP is actually Turing-complete. It’s amazing!
You might have noticed, I had to use the non-standard no_stack_protector
attribute, because clang tries to prevent exactly that. π
4. Writes into Text Section
Usually, you can think of there being 4 different memory regions. We already talked a lot about the stack, which is pre-allocated and is used for local variables, return addresses, and depending on the hardware architecture, ABI and calling convention potentially also arguments and return values.
Additionally, there are the following sections:
- heap (which we will look at later)
- data (for mutable global variables and static locals – yuck!)
- text (for immutable data and the program itself)
The text section (which, among other thing, contains all string literals), is usually read only. Any write access will cause a segfault.
int main(void) {
const char* text = "hello, World!";
char* mutableText = (char*) text;
mutableText[0] = 'H';
return 0;
}
Code language: C++ (cpp)
$ gcc -std=c11 -O0 -Wall -Wpedantic main.c
$ ./a.out
Segmentation fault (core dumped)
I had to execute this example on Linux (gcc 11.4.0, Linux 5.15.0, aarch64) as Darwin gave me a bus error instead:
$ ./a.out
[1] 51703 bus error ./a.out
Bus errors are similar to segfaults. The difference is that bus error typically indicate a problem with the memory layout, like an alignment error or similar.
5. Uninitialised Pointers
The value of uninitialised local variables is undefined. So, if we try to access an uninitialised pointer we might end up at a random address. In reality however, we can control which address will be accessed by setting the corresponding position in the stack beforehand.
In the example below we set the address to NULL
, so that the pointer access fails.
#include <stdio.h>
void presetStack(void) {
int* pointer = NULL;
fprintf(stderr, "preset stack (%p)\n", (void*) pointer);
}
void accessUninitialised(void) {
int* pointer;
fprintf(stderr, "%d\n", *pointer);
}
int main(void) {
presetStack();
accessUninitialised();
return 0;
}
Code language: C++ (cpp)
$ clang -std=c11 -O0 -Wall -Wpedantic main.c
main.c:10:27: warning: variable 'pointer' is uninitialized when used here [-Wuninitialized]
fprintf(stderr, "%d\n", *pointer);
^~~~~~~
main.c:9:14: note: initialize the variable 'pointer' to silence this warning
int* pointer;
^
= NULL
1 warning generated.
$ ./a.out
preset stack (0x0)
[1] 53105 segmentation fault ./a.out
Code language: JavaScript (javascript)
6. Heap Buffer Overflow
Heap buffers are different from stack buffers in that they are (usually) not adjacent to any return addresses or similar. However, since the heap is finite we might end up reading past its bounds.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char* buffer = malloc(1);
fprintf(stderr, "buffer: %p\n\n", (void*) buffer);
for (int i = 0;; i++) {
fprintf(stderr, "\033[1Aend of heap: %p\n", (void*) (buffer + i));
if (buffer[i] == 42) {
// use value; otherwise the compiler will complain
fprintf(stderr, "nice\n");
}
}
return 0;
}
Code language: C++ (cpp)
$ gcc -std=c11 -O0 -Wall -Wpedantic main.c
$ ./a.out
buffer: 0xaaaae8c5b2a0
end of heap: 0xaaaae8c7c000
Segmentation fault (core dumped)
This example I also had to run on Linux. For some reason, Darwin apparently just made the virtual memory bigger every time the pointer went out of bounds, which is… weird. See the image below (iTerm2 is my terminal emulator; the memory usage on the right contains sub-processes – like ./a.out
– as well).
7. Inexecutable Trampoline
Fun fact: GNU C actually supports closures… sorta. You can construct nested functions, that capture the parent scope, and use them as function pointer arguments for other functions.
This is done by means of trampolines: Fragments of executable code on the stack that make sure the nested function has access to the stack frame of the parent. The resulting function pointer is actually a pointer to the trampoline. Of course, this technique requires that the stack-frame of the parent still exists. Therefore, we can not return these nested functions.
Regarding this article, trampolines are interesting, because they require an executable stack. So, let’s see what happens when the stack is not executable:
#include <stdio.h>
#define lambda(r, f) ({r __fn__ f __fn__; })
// f2 is to prevent the compiler from inlining the
// callback function
void f2(void (*callback)(int)) {
callback(42);
}
// f1 is to prevent the compiler from using a regular
// function pointer instead of a trampoline
void f1(int b) {
f2(
lambda(void, (int a) {
fprintf(stderr, "%d\n", a + b);
})
);
}
int main(void) {
f1(1337);
return 0;
}
Code language: C++ (cpp)
$ gcc -std=gnu11 -O0 -Wall -Wpedantic main.c
main.c: In function βf1β:
main.c:15:24: warning: ISO C forbids nested functions [-Wpedantic]
15 | lambda(void, (int a) {
| ^~~~
main.c:3:24: note: in definition of macro βlambdaβ
3 | #define lambda(r, f) ({r __fn__ f __fn__; })
| ^
main.c:3:22: warning: ISO C forbids braced-groups within expressions [-Wpedantic]
3 | #define lambda(r, f) ({r __fn__ f __fn__; })
| ^
main.c:15:17: note: in expansion of macro βlambdaβ
15 | lambda(void, (int a) {
| ^~~~~~
/usr/bin/ld: warning: /tmp/cc8fjTmn.o: requires executable stack (because the .note.GNU-stack section is executable)
$ execstack --clear-execstack a.out
$ ./a.out
[1] 6990 segmentation fault (core dumped) ./a.out
This example also has some special requirements: Nested functions (and statement expressions) don’t exist in ISO C, and require gcc
and the GNU extensions. Additionally, execstack
doesn’t work with aarch64 ELFs. (It’s literally harder to break this on ARM – wtf? π
)
I used gcc 13.2.1 on Linux 6.4.12, x68_64 for this example.
Encore: Segfault Segfault
When I showed this blog post to a friend of mine who is researching low-level security vulnerabilities, they came up with another idea: What if the SIGSEGV
handler causes another segfault?
In the example below we set the SIGSEGV
handler to NULL
. Whenever the signal handler is called, the program immediately jumps into invalid memory.
(Fun fact: Jumping to address 0 does not always crash the program. Some microcontroller architectures, like Atmel AVR for example, store the Interrupt Vector Table at that address. Specifically, address 0 often contains the reset vector. Calling into it will therefore just restart the controller.)
#include<signal.h>
#include<stdlib.h>
#include<stdio.h>
int main(void) {
fprintf(stderr, "set signal handler\n");
signal(SIGSEGV, NULL);
fprintf(stderr, "call NULL\n");
((void (*)(void)) NULL)();
return 0;
}
Code language: C++ (cpp)
$ gcc -std=gnu11 -O0 -Wall -Wpedantic -ggdb main.c
$ ./a.out
set signal handler
call NULL
[1] 8725 segmentation fault (core dumped) ./a.out
Code language: JavaScript (javascript)
Okay, so the program crashes. But what exactly is going on here? Was the signal handler even called? I tried to run gdb
to see what is happening under the hood.
$ gdb ./a.out overflow@friedchicken
GNU gdb (GDB) 13.2
[...]
(gdb) break main
Breakpoint 1 at 0x114d: file main.c, line 6.
(gdb) run
Starting program: ./a.out
[...]
Breakpoint 1, main () at main.c:6
6 fprintf(stderr, "set signal handler\n");
(gdb) step
set signal handler
7 signal(SIGSEGV, NULL);
(gdb) step
9 fprintf(stderr, "call NULL\n");
(gdb) step
call NULL
10 ((void (*)(void)) NULL)();
(gdb) step
Program received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ?? ()
(gdb) continue
Continuing.
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb)
Code language: PHP (php)
I ran everything on Linux, x86_64.
(TIL: gdb
does not support aarch64 on Darwin yet. While trying to figure out why brew
didn’t let me install gdb
, I also found out that even the Darwin aarch64 build of gcc
is only experimental. Wat. π³)
So, it looks like the signal handler is actually called. Though, I’m not really sure why the program terminates. I expected that the program runs into an endless loop of segmentation faults.
I verified that SIGSEGV
can be ignored by setting up an empty signal handler:
#include<signal.h>
#include<stdlib.h>
#include<stdio.h>
void signalHandler(int signal) {
}
int main(void) {
fprintf(stderr, "set signal handler\n");
signal(SIGSEGV, &signalHandler);
fprintf(stderr, "call NULL\n");
((void (*)(void)) NULL)();
for(;;);
return 0;
}
Code language: C++ (cpp)
In this case the program never terminates. So trapping the signal seems to work. Next, I tried to cause a segmentation fault inside the signal handler:
void signalHandler(int signal) {
((void (*)(void)) NULL)();
}
int main(void) {
fprintf(stderr, "set signal handler\n");
signal(SIGSEGV, &signalHandler);
fprintf(stderr, "call NULL\n");
((void (*)(void)) NULL)();
for(;;);
return 0;
}
Code language: C++ (cpp)
This one also crashes the program, similar to the first case where we just set an invalid handler. It seems, Linux just kills the process when the handler causes another fault – I learned something new today. π
Conclusion
Recently, some random streamer (Hi, Prime! π Love your content!) reacted to some of my posts and didn’t like that I end all my articles with my blog’s tagline. But no worries, I hear you. This one will be different. π
So you might be thinking: “C must be a horrible language. There is so much that can go wrong.” And I kinda agree. It’s a low level language after all. However, most of these issues would be caught by static analysis or bounded model checkers like CBMC.
Okay, that’s it. I’ll leave it to Sisu, my pet axolotl, to say good-bye. π