c++x86-64coroutineinline-assemblysetjmp

Why calling longjmp in a non-main stack causes the program to crash?


The following code attempts to create a simple stackful coroutine. It allocates a stack frame in the heap space by setting the rsp register and then calling a function. Afterwards, it exits the coroutine via longjmp. However, using longjmp on the coroutine frame (the coro_test function) will cause a crash, regardless of whether the setjmp is located in the main stack or the coroutine stack.

So, my question is:

My compiler is GCC 14.2.0 x86_64-w64-mingw32.

#include <iostream>
#include <functional>
#include <cstdint>
#include <csetjmp>
#include <memory>
#include <cstdint>

struct coroutine_context
{
    std::jmp_buf exit;

    std::size_t stack_memory_size = 0;
    std::unique_ptr<std::byte[]> stack_memory;
    std::function<void(coroutine_context&)> coroutine_body;

    int value_thoroughfare = 0;
};

#define SET_RSP(new_rsp) \
    asm volatile (       \
        "mov %0, %%rsp"  \
        :                \
        : "r" (new_rsp)  \
        : "memory"       \
    )
    
std::uintptr_t get_stack_base(const coroutine_context& context)
{
    const static bool towards_address_LSB = []() -> bool
    {
        volatile int a;
        volatile int b;
        return &a > &b;
    }();

    if(towards_address_LSB)
        return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get() + context.stack_memory_size) & (~0xF));
    else
        return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get()) & (~0xF)) + 0x10;
}


#if 0
std::jmp_buf coro_jmp_buf;

void coro_test(coroutine_context& context)
{
    if(setjmp(coro_jmp_buf) == 0)
        // Note: Even if the position where setjmp is called is within a coroutine, longjmp will crash after being called.
        std::longjmp(coro_jmp_buf, 1);

    context.value_thoroughfare = 114514;
    std::longjmp(context.exit, 1);
}
#else
void coro_test(coroutine_context& context)
{
    context.value_thoroughfare = 114514;
    std::longjmp(context.exit, 1); // The program crashes here.
}
#endif

void create_coroutine_stack_frame(coroutine_context& context)
{
    if(setjmp(context.exit))
        return;

    SET_RSP(get_stack_base(context));
    coro_test(context);    
}

int main()
{
    coroutine_context context;

    context.stack_memory_size = 1024 * 16;
    context.stack_memory = std::make_unique<std::byte[]>(1024 * 16);
    context.coroutine_body = coro_test;

    create_coroutine_stack_frame(context);
    std::cout << context.value_thoroughfare;
    return 0;   
}

The #if 0 part of the above code shows another crash case, with setjmp and longjmp both called inside the same coroutine so it's not switching stacks.


Solution

  • Answer to Q1:
    You're right that setjmp saves registers and longjmp restores them, but they assume stack continuity. Calling longjmp from a fake, heap-allocated stack breaks this, causing a crash.

    Answer to Q2:
    Use a proper context switching mechanism (assembly, Boost.Context, Windows Fibers). Do not use setjmp/longjmp for coroutine stack switching — it’s unsafe and non-portable.