Memory Safe Context Switching
Memory Safe Context Switching
Fil-C provides a robust implementation of context switching that prioritizes memory safety. While support for ucontext APIs was introduced in release 0.680, users wishing to experiment with setcontext, getcontext, makecontext, and swapcontext must download a binary build the project from source.
Overview of Supported APIs
Fil-C ensures that the following functions are implemented without compromising the system's capability model or risking stack corruption:
- Exception Handling:
setjmpandlongjmp(often used for "throwing" exceptions from signal handlers). - Concurrency Primitives: The
ucontextsuite (getcontext,setcontext,makecontext,swapcontext), which are essential for building fibers and coroutines (e.g., as seen in the Boost library).
[!IMPORTANT] Note that
ucontextAPIs are less common today and have been on certain platforms, such as Darwin.
The Danger of Context Switching
The primary challenge in implementing these APIs is preventing the restoration of a dangling stack. Memory unsafety typically occurs through the following misuse patterns:
- Scope Escape: Calling
setjmporgetcontextinside a function and then returning from that function. The saved context now points to a stack frame that no longer exists. - Manual Deallocation: Using
makecontextto assign a stack, freeing that stack, and subsequently attempting toswapcontextorsetcontextback to it. - Self-Swapping: Passing the currently executing context as the second argument to
swapcontext(often due to argument transposition).
Comparison: Yolo-C vs. Fil-C
| Feature | Yolo-C Behavior | Fil-C Behavior |
|---|---|---|
| Dangling Stack | May result in execution on freed memory. | Impossible. |
| Crash Type | Confusing crashes; no stack trace available. | Immediate panic at the point of misuse. |
| Security | Potential for arbitrary code execution. | Guaranteed memory safety. |
| Outcome | Either a panic or legal execution. |
Deep Dive: The "Evil" Nature of setjmp
To understand how Fil-C secures these operations, we must first acknowledge why setjmp is fundamentally problematic.
"The depravity of
setjmplies in the fact that it returns twice."
Execution Flow Diagram
Analysis of a Simple Program
Consider the following implementation:
#include <setjmp.h>
#include <stdio.h>
int main(int argc, char** argv) {
volatile int x = 42; // Must be volatile!
jmp_buf jb;
if (setjmp(jb)) {
printf("x = %d\n", x);
return 0;
}
x = 666;
longjmp(jb, 1);
printf("Should not get here.\n");
return 1;
}
Expected Output: x = 666
The Role of volatile
Without the volatile keyword, the compiler may optimize the variable , causing the program to print 42 instead of 666. This happens because:
- Constant Folding: The compiler may simply replace with the constant .
- Register Allocation: might be stored in a callee-save register that was captured during the first
setjmpcall. - Spill Slot Splitting: At optimization levels above
-O0, the compiler might treat and as two distinct variables with separate memory locations (spill slots). Theprintfcall might reference the first slot because it dominates the control flow.
Critical Reflections on Stack Allocation
To ensure the updated value is observed, the compiler must treat as a consistent stack allocation. This leads to several technical questions:
- How does the compiler ensure a stack allocation isn't reused?
- Why wouldn't the spill slot be overwritten by the time
longjmpoccurs? - Can passing a pointer to force the compiler to use a single stack location?
The "Diabolical" Example
To prove the issue of spill slot splitting, one can force register pressure in gcc, clang, or filcc as follows:
#include <setjmp.h>
#include <stdio.h>
int main(int argc, char** argv) {
int x = 42;
asm volatile("" : "+r"(x));
jmp_buf jb;
// Inducing register pressure to force spilling
int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 9, i = 10;
asm volatile("" : "+r"(a), "+r"(b), "+r"(d), "+r"(e), "+r"(