NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...6 min read

Memory Safe Context Switching

Share
NOW LET US Article – Memory Safe Context Switching

This article explores how Fil-C achieves absolute memory safety during context switching using setjmp/longjmp and ucontext APIs, preventing stack corruption and dangling stack exploits.

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

andsetjmp

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

, andswapcontext

(aka theucontext

APIs) are used to implement coroutines and fibers. For example, Boost usesucontext

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 eitherswapcontext

orsetcontext

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 alongjmp

; 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 <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 prints:

x = 666

And then exits. The flow is:

  • On the first call to setjmp

, it returns 0 and saves its caller's context injb

. - Then we set x

to 666 andlongjmp

tojb

with the value 1. setjmp

returns 1, so weprintf

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 removevolatile

and use any optimization level above-O0

. Thenx = 42

gets printed. - Say that constant folding doesn't happen, maybe because we insert a asm("" : "+r"(x))

right after the definition ofx

. In that case, the compiler could register-allocatex

in a callee-save register, in which case the register ends up saved bysetjmp

. This also leads tox = 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 splitx

into two variables: one forx = 42

and one forx = 666

, and the printf will reference the first one (sincex = 42

dominates theprintf

). Those two variables willalmost alwaysget separate spill slots. Hence, when we come out of thesetjmp

the second time, readingx

will still give 42.

Three things to reflect upon:

  • To get the property that x

's value is observed to be 666 in the printf, we need to make sure that the compiler treatsx

as a stack allocation rather than a variable. Usingvolatile

achieves this. Also, passing a pointer tox

to anywhere is likely to accomplish this. - Spill slots are not the same as stack allocations. If a variable is stack-allocated, then it will get one stack allocation. If a variable is spilled, it may get multiple spills (often, a separate spill per assignment).

  • The compiler is allowed to analyze the lifetime of spill slots and stack allocations. It's allowed to reuse spill slots. How does the compiler know that the x = 42

spill slot should stay alive until thelongjmp

happens? How come it won't get reused, resulting inx

having either 666 or any random garbage when we fall out of thesetjmp

a second time?

Here's a more diabolical version of the example that triggers spilling of x

to two different spill slots (one for 42 and one for 666) in gcc, clang, and filcc.

#include <setjmp.h>
#include <stdio.h>
int main(int argc, char** argv)
{
int x = 42;
asm volatile("" : "+r"(x));
jmp_buf jb;
int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 9, i = 10;
/* Force some spilling */
asm volatile("" : "+r"(a), "+r"(b), "+r"(d), "+r"(e), "+r"(f), "+r"(g), "+r"(h), "+r"(i));
if (setjmp(jb)) {
asm volatile("" : "+r"(a), "+r"(b), "+r"(d), "+r"(e), "+r"(f), "+r"(g), "+r"(h), "+r"(i));
printf("x = %d\n", x);
return 0;
}
x = 666;
void (*jump)(jmp_buf, int) = longjmp;
asm volatile("" : "+r"(x));
asm volatile("" : "+r"(jump), "+r"(a), "+r"(b), "+r"(d), "+r"(e), "+r"(f), "+r"(g), "+r"(h), "+r"(i));
jump(jb, 1);
asm volatile("" : "+r"(jump), "+r"(a), "+r"(b), "+r"(d), "+r"(e), "+r"(f), "+r"(g), "+r"(h), "+r"(i));
asm volatile("" : "+r"(x));
printf("Should not get here.\n");
return 1;
}

This program will print x = 42

even though x

is not constant folded or register-allocated.

Note that all of the examples so far work in Fil-C. Even the inline assembly that we're using to obfuscate variable values works in Fil-C, and has the desired effect.

What Is Even Happening

Let's take a look at how simple setjmp

is by looking at the musl implementation on x86_64:

__setjmp:
_setjmp:
setjmp:
mov %rbx,(%rdi) /* rdi is jmp_buf, move registers onto it */
mov %rbp,8(%rdi)
mov %r12,16(%rdi)
mov %r13,24(%rdi)
mov %r14,32(%rdi)
mov %r15,40(%rdi)
lea 8(%rsp),%rdx /* this is our rsp WITHOUT current ret addr */
mov %rdx,48(%rdi)
mov (%rsp),%rdx /* save return addr ptr for new rip */
mov %rdx,56(%rdi)
xor %eax,%eax /* always return 0 */
ret

This is only saving the callee-save registers, plus the stack pointer and instruction

© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

NOW LET US Related – The end of the AArch64 desktop experiment

dev-tools

The end of the AArch64 desktop experiment

After about eleven months of using an Ampere Altra AArch64 system as a desktop, the author decided to end the experiment due to hardware errata, GPU driver issues, and software ecosystem limitations.

NOW LET US Related – .self: A new top-level domain designed to support self-hosting

dev-tools

.self: A new top-level domain designed to support self-hosting

The Human-Centered Computing Foundation has launched a campaign for '.self', a new top-level domain dedicated to ethical, self-hosted technology. However, the initiative's decision to publish its manifesto as a PDF has sparked a heated debate among developers about web standards and usability.

NOW LET US Related – DiScoFormer: One transformer for density and score, across distributions

dev-tools

DiScoFormer: One transformer for density and score, across distributions

DiScoFormer is a novel Transformer-based model that estimates both the density and score of a distribution in a single forward pass, eliminating the need for retraining and overcoming traditional trade-offs in generative modeling.

NOW LET US Related – Qwen 3.6 27B is the sweet spot for local development

dev-tools

Qwen 3.6 27B is the sweet spot for local development

Qwen 3.6 27B is emerging as the go-to choice for developers looking to run large language models locally. Offering superior performance and impressive optimization via llama.cpp, it represents the perfect sweet spot between speed and output quality.

NOW LET US Related – WATaBoy: JIT-Ing Game Boy Instructions to WASM Beats a Native Interpreter

dev-tools

WATaBoy: JIT-Ing Game Boy Instructions to WASM Beats a Native Interpreter

An exploration of WATaBoy, a Game Boy emulator that bypasses iOS JIT restrictions by compiling instructions to WebAssembly at runtime, achieving better performance than a native interpreter.

NOW LET US Related – Linux for the Sega MegaDrive

dev-tools

Linux for the Sega MegaDrive

A developer has successfully ported and booted the Linux kernel on the classic 16-bit Sega MegaDrive (Genesis) console using a custom toolchain and the Mega EverDrive cartridge.

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.