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:

  1. Dangling Stacks: If a program calls setjmp and then returns from that function, the saved context becomes invalid. A subsequent longjmp to that context attempts to restore a stack frame that no longer exists, leading to a torn machine state.
  2. Compiler Optimization Errors: Because setjmp can return twice, compilers must disable certain optimizations (like spill slot reuse) for functions calling it. If a compiler is unaware that a function is calling setjmp (e.g., via a function pointer), it may reuse spill slots, causing variables to hold incorrect or garbage values after a longjmp.

Fil-C's Implementation Strategy

Fil-C mitigates these risks through the following mechanisms:

  • Opaque Jump Buffers: The jmp_buf contains a pointer to an opaque zjmp_buf object 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) requires setjmp to 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's returns_twice logic always triggers.
  • Stack Frame Registration: When setjmp is called, it allocates a zjmp_buf and registers it with the current stack frame. Each frame maintains a weak set of valid zjmp_buf targets.
  • Ancestor Validation: longjmp will panic unless the current stack frame is a descendant of the frame that created the zjmp_buf (meaning the target frame is an ancestor on the current call stack). This is verified by walking the stack and checking for the zjmp_buf in the frame's valid set.
  • GC Integration: zjmp_buf stores a copy of the GC roots of the frame at the time of setjmp. As long as the zjmp_buf is 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 getcontext or use as the from argument in swapcontext is permitted.
  • After_getcontext: State after getcontext returns. Only makecontext or use as the from argument in swapcontext is permitted.
  • Runnable: State after makecontext or after being the from argument in swapcontext. Only setcontext or use as the to argument in swapcontext is permitted.
  • Running: State when the context is currently executing. Only swapcontext is 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) in ucontext_t. Instead, the runtime allocates a managed stack internally during makecontext, ensuring the stack is compatible with Fil-C's overflow handling.
  • Thread Affinity: To maintain ABI consistency, zfiber_context tracks 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_t contains a pointer to an opaque zfiber_context object, 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

Sources