Fil-C Memory Safe Context Switching
Fil-C Memory Safe Context Switching
Fil-C provides memory-safe implementations of setjmp, longjmp, and the ucontext APIs (getcontext, setcontext, makecontext, and swapcontext). These mechanisms ensure that misuse of context-switching APIs cannot lead to stack corruption or violations of the Fil-C capability model, specifically preventing the restoration of dangling stacks.
Memory-Safe setjmp and longjmp
Fil-C prevents the common memory safety pitfalls of setjmp and longjmp by treating the jump buffer as an opaque object and enforcing strict stack-frame validation.
The Danger of Standard setjmp/longjmp
In standard C, setjmp saves the register state (callee-save registers, stack pointer, and instruction pointer) but does not save the stack itself. This creates two primary safety risks:
- Dangling Stacks: If a program calls
setjmpand then returns from that function, the saved context becomes invalid. A subsequentlongjmpto that context attempts to restore a stack frame that no longer exists, leading to a torn machine state. - Compiler Optimization Errors: Because
setjmpcan return twice, compilers must disable certain optimizations (like spill slot reuse) for functions calling it. If a compiler is unaware that a function is callingsetjmp(e.g., via a function pointer), it may reuse spill slots, causing variables to hold incorrect or garbage values after alongjmp.
Fil-C's Implementation Strategy
Fil-C mitigates these risks through the following mechanisms:
- Opaque Jump Buffers: The
jmp_bufcontains a pointer to an opaquezjmp_bufobject managed solely by the Fil-C runtime. This prevents users from spoofing or manually manipulating the jump state. - Compiler Enforcement: The Fil-C compiler (
filcc) requiressetjmpto be called directly. Any attempt to obfuscate the call (e.g., using a function pointer) results in a compiler error or internal compiler error (ICE), ensuring the compiler'sreturns_twicelogic always triggers. - Stack Frame Registration: When
setjmpis called, it allocates azjmp_bufand registers it with the current stack frame. Each frame maintains a weak set of validzjmp_buftargets. - Ancestor Validation:
longjmpwill panic unless the current stack frame is a descendant of the frame that created thezjmp_buf(meaning the target frame is an ancestor on the current call stack). This is verified by walking the stack and checking for thezjmp_bufin the frame's valid set. - GC Integration:
zjmp_bufstores a copy of the GC roots of the frame at the time ofsetjmp. As long as thezjmp_bufis live, the GC continues to mark those roots.
Memory-Safe ucontext APIs
Support for ucontext APIs was introduced in Fil-C release 0.680. These APIs are used to implement fibers and coroutines by allowing the saving and restoring of execution contexts.
The ucontext State Machine
To prevent misuse, Fil-C implements a restricted state machine for zfiber_context objects:
- Uninitialized: The initial state. Only
getcontextor use as thefromargument inswapcontextis permitted. - After_getcontext: State after
getcontextreturns. Onlymakecontextor use as thefromargument inswapcontextis permitted. - Runnable: State after
makecontextor after being thefromargument inswapcontext. Onlysetcontextor use as thetoargument inswapcontextis permitted. - Running: State when the context is currently executing. Only
swapcontextis permitted, and only if the context is the one currently running on the thread.
Safety Constraints and Implementation
- Stack Management: Fil-C ignores the user-provided
ss_sp(stack pointer) inucontext_t. Instead, the runtime allocates a managed stack internally duringmakecontext, ensuring the stack is compatible with Fil-C's overflow handling. - Thread Affinity: To maintain ABI consistency,
zfiber_contexttracks the thread on which it was created. It prohibits switching to a context from a different thread, preventing stacks from migrating across threads. - Opaque State: Like
jmp_buf,ucontext_tcontains a pointer to an opaquezfiber_contextobject, hiding the internal machine state from the user.
Garbage Collection and Fiber Switching
Integrating fibers with an on-the-fly garbage collector requires careful tracking of stacks that are not currently owned by a thread.
The Grey Fiber Problem
When a fiber is runnable (not currently executing), its stack must be scanned by the GC. However, if a mutator switches to a fiber and then switches away during a GC mark phase, the GC might miss the fiber's stack if it has already marked the fiber as "black" (processed).
Fil-C solves this by tracking grey fibers. When swapcontext is called during a marking phase, the context being switched from is added to the current thread's grey_fibers list. During the final stack rescan before GC termination, the thread walks the stacks of all grey fibers in its list, ensuring no live objects are missed.
Summary of API Support
| API | Fil-C Safety Mechanism | Primary Use Case |
|---|---|---|
setjmp / longjmp |
Ancestor validation & Opaque zjmp_buf |
Exception handling, signal handlers |
getcontext / makecontext |
State machine & Managed stacks | Fiber/Coroutine bootstrapping |
swapcontext |
Thread affinity & Grey fiber GC tracking | Fiber context switching |