Implement setjmp and longjmp functionality with ucontext

101 Views Asked by At

I am trying to grasp some key concepts in order to fully understand the ucontext and setjmp libraries.

On what concerns ucontext:

  • User context, eg program counter and stack register.
  • Is saved on a ucontext_t type when executing getcontext
  • Activates a previously saved context via setcontext (this implies that the context was stored with getcontext)
  • Swaps context with swapcontext by saving the current program state and starts executing by the 2nd context since we will load the previously stored context

Some sample code I tried:

#include <stdio.h>
#include <ucontext.h>
int main() {
  // if you use the register storage identifier
  // the variable will be stored at the CPU
  // and since no change will be noticed it will be swaping forever
  /*register*/ int done = 0;
  ucontext_t one;
  ucontext_t two;

  getcontext(&one);

  printf("hello \n");

  if (!done) {
    printf("about to swap \n");
    done = 1;
    swapcontext(&two, &one);
  }
}

On what concerns setjmp and longjmp:

  • Pretty much a global goto, the caveat is that since we will be jumping outside the current scope, we need to somehow store the state aka context when we jump back
  • setjmp sets the point and returns twice, 0 when it's called from the current execution path and something else via longjmp
  • longjmp practically signals to jump back to the point and set the new return code that setjmp will return.

Some sample code I tried

#include <setjmp.h>
#include <stdio.h>
jmp_buf buf;
void func() {
  printf("about to jump back\n");

  longjmp(buf, 1);

  printf("unreachable\n");
}

int main() {
  // Setup jump position using buf and return 0
  if (setjmp(buf))
    printf("This should be executed because longjmp returned 1 \n");
  else {
    printf("This should be executed because setjmp returned 0\n");
    func();
  }
  return 0;
}

Please, if I have misunderstood something so far feel free to correct me.

On my actual question/problem now:

I am trying to implement my own setjmp and longjmp with ucontext.

Below is how I am trying to do so

#include <stdio.h>
#include <ucontext.h>

typedef struct jump_buffer {
  ucontext_t ctx;
  int is_set;
} jump_buffer_t;

jump_buffer_t buf;

int set_jumping_point(jump_buffer_t *uc_jmp_buf) {
  getcontext(&uc_jmp_buf->ctx);
  uc_jmp_buf->is_set = 0;
  return uc_jmp_buf->is_set;
}

void jump_back(jump_buffer_t *uc_jmp_buf, int value) {
  uc_jmp_buf->is_set = value;
  setcontext(&uc_jmp_buf->ctx);
}

void func() {
  printf("about to jump back\n");

  jump_back(&buf, 1);

  printf("unreachable\n");
}

int main() {
  buf.is_set = 1;
  // Setup jump position using buf and return 0
  if (set_jumping_point(&buf))
    printf("This should be executed because longjmp returned 1 \n");
  else {
    printf("This should be executed because setjmp returned 0\n");
    func();
  }
  return 0;
}

Theoretically, my output should be

This should be executed because setjmp returned 0
about to jump back
This should be executed because longjmp returned 1

But my actual output is

This should be executed because setjmp returned 0
about to jump back
Segmentation fault (core dumped)

After debugging the program, I saw that the segfault occurs inside set_jumping_point

uc_jmp_buf->is_set = 0;

First question, why? I am assuming that since I set the context before entering function, by struct was initialized, so even when I am going back, the struct should not be corrupted, why the segfault?

I then tried commenting out that line but then the function returned normally, which means that it was able to read the struct and it is not supposed to be corrupted in that case? For the record, I am attaching the output of the code snippet with the segfaulty line commented out

This should be executed because longjmp returned 1

Can someone explain what am I missing in the bigger picture? It feels weird for that to be a segfault so I am assuming that I am misusing ucontext, but now sure where.

Edit : practically, setjmp does not return properly twice

2

There are 2 best solutions below

0
John Omielan On

There are several mistakes in your last set of code. Regarding the segmentation fault, the main issue is that when execution returns to the getcontext call within setjumpingpoint after calling setcontext, the function stack values are not the same anymore. This is because the call to getcontext does not store the entire stack contents, so setcontext doesn't then restore them later. This is because, as shown in Wikipedia's setcontext article, we have

typedef struct {
    ucontext_t *uc_link;
    sigset_t    uc_sigmask;
    stack_t     uc_stack;
    mcontext_t  uc_mcontext;
    ...
} ucontext_t;

The stack related information is stored in the stack_t object. As stated in this answer to Is the type `stack_t` no longer defined on linux?,

The header defines the stack_t type as a structure that includes at least the following members:

void     *ss_sp       stack base or pointer
size_t    ss_size     stack size
int       ss_flags    flags

In particular, although the size and stack pointer are stored, the entire stack contents are not. This means that, without using a call to makecontext to use a different stack, the call to setcontext will basically just unwind the stack back to where getcontext was last used. Thus, it should only be used to return to somewhere within the current function, or to one of the parent functions going up the call stack chain.

To make this problem more explicit, although call stacks may be implemented differently depending on the machine and compiler, assume it's done as shown in the Structure section of Wikipedia's "Call stack" article. When getcontext is called within set_jumping_point, from the top-down we would then have

stack pointer
return address to main
uc_jmp_buf pointer

After that call finishes initially, the stack would unwind back. Then the call to func, since it contains no parameters, would change the call stack to

stack pointer
return address to main

Note the uc_jmp_buf pointer has now been overwritten by the return address to main! The call to jump_back would add to the top of the stack, but not change any of the lower level values. Thus, the call to setcontext will, among other things, cause the stack pointer to return where it was when the getcontext was called, so then trying to execute the line

uc_jmp_buf->is_set = 0;

will cause the code to use the return value to main as the pointer uc_jmp_buf, thus causing the segmentation fault.


Next, note that in your set_jumping_point function, the last 2 lines are

  uc_jmp_buf->is_set = 0;
  return uc_jmp_buf->is_set;

The member value of is_set is unconditionally set to 0, and that value is returned in the next line. Thus, set_jumping_point will always return 0. There are several ways to change your code to do what you want, with I believe the simplest being to remove the uc_jmp_buf->is_set = 0; line and change the buf.is_set = 1; line to be buf.is_set = 0; instead. This way, the initial value returned from set_jumping_point will be 0, while because the value of is_set changes to 1 in the jump_back function, the second return value from set_jumping_point will then be 1.

One final point is that, as stated in the setcontext article, regarding the getcontext function,

... the programmer must use an explicit flag variable, which must not be a register variable and must be declared volatile to avoid constant propagation or other compiler optimizations.

Thus, there's a chance that, depending on which compiler, and even which version, you're using that the program will not always work properly. As such, I suggest declaring the appropriate variable to be volatile.


Regarding how to fix the main issue of the segmentation fault occurring, if you're using a C++ compiler, then making the functions inline should usually work since then there's no explicit function calls, and thus changes to the stack, that would cause this problem to occur. However, the inline keyword is only a suggestion, not a requirement, to the compiler, although your functions should not qualify for exclusion from being inlined. Alternative methods to avoid this stack problem is to have the function code be in the main function itself, or to use macros, as suggested and shown in Craig Estey's answer.

Another option is to, such as shown in the Example section of Wikipedia's "setcontext" article, use makecontext to have a separate stack be used instead during the calls to the function.

0
Craig Estey On

The issue is that we're calling getcontext from a child function (i.e. set_jumping_point).

getcontext saves the stack frame/pointer for the function that called it. Here, this is the stack frame for set_jumping_point. This disappears (and becomes invalid) when set_jumping_point returns.

So, when we call setcontext, it sets the stack pointer to point to the now invalid/non-existent stack frame for set_jumping_point. This is UB (undefined behavior) and, in this situation, causes a segfault.

We need to save the stack context for the caller function (i.e. main) that persists until we call setcontext.

We can't do this when set_jumping_point is a function. We need to convert it to a macro.

In the code below, I've used a gcc extension to make the macro "pretty". Or, [a bit messier] we just physically cut-and-paste the code from set_jumping_point into main.

Also, we should set is_set before calling getcontext and we should guarantee that the compiler won't reorder the calls, so we should add a barrier.

There would be a similar issue for setjmp if we put it into a separate child function. In your example, main calls setjmp directly, so this is okay. If we put the setjmp into a subfunction (e.g. set_jumping_point_setjmp) and we did: main --> set_jumping_point_setjmp --> setjmp, it would have a similar problem.


Here is the corrected code:

#include <stdio.h>
#include <ucontext.h>

typedef struct jump_buffer {
    ucontext_t ctx;
    int is_set;
} jump_buffer_t;

jump_buffer_t buf;

#define barrier \
    __asm__ __volatile__("" ::: "memory" )

#if 0
int
set_jumping_point(jump_buffer_t *uc_jmp_buf)
{
    getcontext(&uc_jmp_buf->ctx);
    uc_jmp_buf->is_set = 0;
    return uc_jmp_buf->is_set;
}
#else
#define set_jumping_point(_bufp) \
    ({ \
        (_bufp)->is_set = 0; \
        barrier; \
        getcontext(&(_bufp)->ctx); \
        (_bufp)->is_set; \
    })
#endif

void
jump_back(jump_buffer_t *uc_jmp_buf, int value)
{
    uc_jmp_buf->is_set = value;
    setcontext(&uc_jmp_buf->ctx);
}

void
func()
{
    printf("about to jump back\n");

    jump_back(&buf, 1);

    printf("unreachable\n");
}

int
main()
{
    buf.is_set = 1;
    // Setup jump position using buf and return 0
    if (set_jumping_point(&buf))
        printf("This should be executed because longjmp returned 1 \n");
    else {
        printf("This should be executed because setjmp returned 0\n");
        func();
    }
    return 0;
}

Here is the output:

This should be executed because setjmp returned 0
about to jump back
This should be executed because longjmp returned 1