Taming C’s Darkest Arts: Memory-Safe Context Switching in Fil-C
How Fil-C brings memory safety to the chaotic world of setjmp, longjmp, and ucontext fibers.
C's control flow is usually predictable, obeying the strict hierarchy of the call stack. But when you introduce non-local jumps and coroutines, that predictability vanishes. The setjmp/longjmp primitives and the ucontext suite (getcontext, setcontext, makecontext, swapcontext) are the wild west of systems programming. They bypass standard function returns, manipulate registers directly, and invite catastrophic memory safety bugs.
In standard C, which we can call "Yolo-C," misusing these APIs leads to dangling stacks, silent memory corruption, and untraceable crashes. Fil-C, a memory-safe compiler and runtime, tackles this problem head-on. With support for ucontext APIs introduced in release 0.680, Fil-C proves that even C's most volatile control-flow mechanisms can be tamed without sacrificing their utility.
The Dangling Stack Nightmare
To understand why context switching is so dangerous, we have to look at how Yolo-C manages stacks. When you use setjmp or getcontext, the runtime saves the current execution state, including the stack pointer.
If you return from the function that saved the context, or if the host thread exits, that stack frame is gone. Attempting to jump back to that saved context means restoring execution on a stack that has been freed or reassigned.
With ucontext APIs, which are widely used by libraries like Boost to implement fibers and coroutines, the potential for disaster increases. A developer can use makecontext to point to a specific stack, free that stack, and then call swapcontext or setcontext to jump into it.
In Yolo-C, this results in running on a dangling stack. The debugger won't even be able to print a stack trace because the stack pointer points to garbage. Worse, an attacker can exploit these corrupted stack states to hijack control flow entirely.
Fil-C eliminates this class of bugs. By integrating context switching into its capability model, Fil-C ensures that executing on a dangling stack is impossible. If you misuse these APIs, Fil-C either keeps the stack execution reliably legal through its managed stack lifetime or panics the program immediately before corruption can occur.
The Compiler Optimization Trap
To see why retrofitting safety onto these APIs is so difficult, consider the sheer depravity of setjmp. Because it returns twice, it completely breaks the assumptions that modern optimizing compilers make about variable lifetimes.
Take this classic example:
#include <setjmp.h>
#include <stdio.h>
int main(int argc, char** argv) {
volatile int x = 42;
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;
}
This program should print x = 666 and exit. But to guarantee this behavior, x must be marked volatile. Without volatile, any optimization level above -O0 allows the compiler to break the program in several ways:
- Constant Folding: The compiler might optimize the
printfto always output 42, assumingxcannot change between thesetjmpcall and the print statement. - Register Allocation: The compiler might store
xin a callee-save register. Sincesetjmpsaves and restores registers, the register is reset to its pre-jump state, restoring the value 42. - Spill Slot Splitting: The compiler may split
xinto two separate variables (one for 42, one for 666) and assign them different spill slots on the stack. Because compilers reuse spill slots aggressively, the slot holding 42 might be overwritten by the timelongjmpreturns, resulting in garbage.
Compilers simply are not built to reason about backward jumps over arbitrary lifetimes. They treat spill slots differently than stack allocations, and they assume standard call-and-return semantics.
How Fil-C Enforces Safety
Fil-C solves these issues by changing how stacks and contexts are represented. Instead of relying on raw pointers and registers, Fil-C uses a capability model.
When you use makecontext in Fil-C, the context object holds a strong capability to the allocated stack. Even if you attempt to free the stack in your code, the runtime keeps the underlying memory alive as long as a valid context capability points to it. This prevents the dangling stack scenario entirely.
Fil-C also adds strict validation to context transitions. For example, a common developer error is calling swapcontext with the second argument (the target context) being the currently executing context. In Yolo-C, this can trigger a pseudo-longjmp or corrupt the stack. Fil-C detects this self-swap at runtime and immediately panics the program, preventing undefined behavior.
This safety extends to setjmp and longjmp. Fil-C tracks the validity of the jump buffer's target frame. If a program attempts to longjmp to a frame that has already returned, Fil-C catches the violation and aborts execution safely.
The Developer Trade-Off
For developers maintaining legacy C codebases, Fil-C's context-switching support is a major milestone. It allows you to run complex fiber libraries and exception-handling code with modern memory safety guarantees.
However, adopting this model requires understanding the trade-offs:
- Build Requirements: Support for the
ucontextAPIs is new since release 0.680. If you want to usesetcontext,getcontext,makecontext, andswapcontext, you must build Fil-C from source. - Performance Overhead: Managing stack lifetimes via capabilities and performing runtime validation on every context switch adds CPU overhead. For high-performance fiber runtimes, this tax might be noticeable.
- Behavioral Changes: Code that relied on undefined but "working" hacks in Yolo-C will now panic. Fil-C turns silent corruption into loud, immediate failures. While this is the correct behavior for security, it means you may have to debug and refactor legacy context-switching logic that was secretly broken.
Ultimately, Fil-C proves that you do not have to rewrite your entire C stack in Rust to get memory safety. By taming C's most chaotic control-flow primitives, it provides a viable path forward for securing legacy infrastructure.
Sources & further reading
- Memory Safe Context Switching — fil-c.org
Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.
Discussion 1
i've always been intimidated by setjmp and longjmp, so it's really cool to see fil-c tackling memory safety issues with these primitives - does anyone know if fil-c is planning to support other languages besides c?