Memory Safe Context Switching
Home
Installing
Documentation
Releases
GitHub
Meet Fil
Memory Safe Context Switching
Support for ucontext APIs is new since release 0.680. If you want to play with setcontext, getcontext, makecontext, and swapcontext then you have to build from source.
This document describes how Fil-C supports longjmp, setjmp, setcontext, getcontext, makecontext, and swapcontext in a totally memory-safe way. In particular, no misuse of those APIs in Fil-C can lead to stack corruption or any other violation of Fil-C's capability model.
These APIs are widely used:
longjmp and setjmp are used in C programs to implement exception handling. It's especially common to use them to implement exceptions "thrown" from signal handlers.
getcontext, setcontext, makecontext, and swapcontext (aka the ucontext APIs) are used to implement coroutines and fibers. For example, Boost uses ucontext as part of its fiber implementation.
The ucontext APIs are less commonly used than longjmp/setjmp and some OSes (like Darwin) have deprecated them. However, they remain well supported in glibc.
Implementing these APIs in a way that preserves memory safety is hard since their misuse can result in restoring a dangling stack. For example, you could either setjmp or getcontext within some function, and then do any of the following things:
Return from that function. At this point, the context that was saved will attempt to restore a stack frame that no longer exists.
Exit from the thread. At this point, the context that was saved will attempt to restore execution on a stack that has been freed.
Even more friendly APIs like makecontext and swapcontext can be straightforwardly misused:
You can use makecontext to create a context that points to some stack, then free that stack, and then either swapcontext or setcontext to that context. In Yolo-C, this will result in running on a dangling stack. Fil-C makes this not an error.
You can call swapcontext with the second argument being the context that is currently executing. This might happen if you confuse the first and second arguments. In Yolo-C, in the best case, this will behave like a longjmp; in the worst case, it will result in executing on a dangling stack. In Fil-C, this is a safety error that panics your program.
In Yolo-C, execution on a dangling stack results in the most confusing kinds of crashes, since the debugger won't even be able to print a stack trace! Worse, if the program has subtle bugs in its handling of contexts, then an attacker could exploit those bugs to cause the program to do whatever the attacker likes. In Fil-C, execution on a dangling stack is not possible: all such cases are either panics at the point where you misused longjmp or one of the ucontext APIs, or they are reliably legal execution because of how Fil-C manages stacks.
Fil-C implements setjmp/longjmp and the ucontext APIs quite differently.
Making setjmp/longjmp Memory Safe
There is an impressive amount of depth to the depravity of setjmp. Before going into the details of how Fil-C implements setjmp/longjmp, we need to discuss exactly what makes this function so amazingly evil.
setjmp saves the context as it was at the moment when it was called so that when longjmp is called later, setjmp will return a second time. It is the fact that it returns twice that makes it so vile, and so we need to understand the implications precisely.
An Example
Consider this simple program:
#include<br>#include
int main(int argc, char** argv)<br>volatile int x = 42;<br>jmp_buf jb;<br>if (setjmp(jb)) {<br>printf("x = %d\n", x);<br>return 0;<br>x = 666;<br>longjmp(jb, 1);<br>printf("Should not get here.\n");<br>return 1;
This program prints:
x = 666
And then exits. The flow is:
On the first call to setjmp, it returns 0 and saves its caller's context in jb.
Then we set x to 666 and longjmp to jb with the value 1.
setjmp returns 1, so we printf and exit.
Note that we have to mark x as volatile for the program to reliably print 666. Otherwise, the compiler is allowed to optimize the access to x and have it return 42 instead. This might happen in the following ways:
The compiler could constant fold x to 42. This will happen in the example if we remove volatile and use any optimization level above -O0. Then x = 42 gets printed.
Say that constant folding doesn't happen, maybe because we insert a asm("" : "+r"(x)) right after the definition of x. In that case, the compiler could register-allocate x in a callee-save register, in which case the register ends up saved by setjmp. This also leads to x = 42 being printed.
Say that we experience register pressure for some reason, and x doesn't make it into a callee-save register, but instead gets spilled. At any optimization level above -O0, the compiler will split x into two variables: one for x = 42 and one for x = 666, and the printf will reference the first one (since x = 42 dominates the printf). Those two variables will almost always get separate spill slots....