Why longjmp Clobbers Non-Volatile Variables in C

In the C programming language, invoking longjmp can restore or corrupt the values of local automatic variables that were modified after the corresponding setjmp call. This occurs because modern optimizing compilers cache local variables in CPU registers rather than keeping them synchronized in system memory. When longjmp executes, it restores the CPU registers to the exact snapshot captured during setjmp, overwriting any register-cached changes unless the variable is explicitly declared with the volatile type qualifier.

The Mechanics of setjmp and longjmp

The setjmp and longjmp functions provide a mechanism for non-local jumps, bypassing normal function call and return conventions.

  1. setjmp(jmp_buf env): Saves the execution environment—primarily CPU control registers, including the stack pointer (SP), frame pointer (FP), instruction pointer (IP), and any callee-saved registers—into the env buffer.
  2. longjmp(jmp_buf env, int val): Restores the CPU registers from env and transfers control back to the point where setjmp was called, causing setjmp to return a second time with the value val.

Critically, longjmp does not unwind the stack or track operations that occurred between the call to setjmp and the call to longjmp; it simply resets the CPU core state to the point of the snapshot.

Compiler Register Allocation and Clobbering

Compilers optimize code performance by allocating frequently accessed automatic (local) variables to CPU registers rather than reading and writing them to the stack frame in RAM.

When a local variable is modified after setjmp:

  • If the compiler placed that variable into a register, the updated value exists only in that register.
  • The original value of that register was already recorded in jmp_buf during the initial setjmp invocation.
  • When longjmp executes, it overwrites the CPU registers with the values preserved in jmp_buf.
  • As a result, the register holding your local variable is reverted to its pre-setjmp state, discarding any assignments made in the interim.

If the compiler instead stored the variable on the stack frame (which remains in memory because the function containing setjmp has not returned), the variable might retain its modified value. Because register allocation depends entirely on optimization levels (-O0, -O2, etc.) and compiler heuristics, the variable's value becomes non-deterministic without proper qualification.

The C Standard Rule

To account for these architectural realities, the ISO C Standard (ISO/IEC 9899) explicitly defines this behavior. According to the standard, the values of automatic local variables in the function invoking setjmp become indeterminate after a longjmp if they meet all of the following conditions:

  1. They are local to the function containing the setjmp invocation.
  2. They do not have the volatile qualifier.
  3. They are changed between the setjmp invocation and the longjmp call.

Reading such a variable after longjmp returns yields undefined behavior.

Preventing Clobbering with volatile

Declaring a variable as volatile informs the compiler that the variable's value may change outside the normal compiler flow and must not be cached in a CPU register.

#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void trigger_jump(void) {
    longjmp(env, 1);
}

int main(void) {
    int regular_var = 10;
    volatile int volatile_var = 10;

    if (setjmp(env) == 0) {
        regular_var = 20;
        volatile_var = 20;
        trigger_jump();
    } else {
        // regular_var is indeterminate; it may print 10 or 20 depending on optimization.
        // volatile_var is guaranteed to retain 20.
        printf("regular_var: %d\n", regular_var);
        printf("volatile_var: %d\n", volatile_var);
    }

    return 0;
}

Because volatile_var is forced to reside in memory on the stack, every write goes directly to RAM. When longjmp restores the CPU registers, the memory location on the stack remains untouched, allowing the program to safely read the updated value.