The Self-Healing Road to Perdition

☃️
Git-Repository: Template Solution Solution-Diff (Solution is posted at 18:00 CET)
Workload: 89 lines of code
Important System-Calls: sigaction(2)
Recommended Reads:

Something went wrong, terribly wrong. An ELF was working at its workbench and wanted to grab a screwdriver. As he was sure he had placed his tool at the top left of the workbench, he did not look properly, and he grabbed only into thin air. The workbench was shorter than he though, so he stumbled, fell, and... well... there was a running table saw. I will not stress your imagination even more... Let's just say that the ELF is well and alive, but he is still recovering in the ELF hospital. To prevent such accidents in the future, the ELF senate decided to build self-extending workbenches that grows in the right direction if an ELF grabs beside the bench. A very specific solution, to a very specific problem. But as I already said, ELFs are technocrats that want to solve every problem with the right piece of technology.

Signals

Problems are a problem. Sometimes, and especially if you are a beginner C programmer, weird bugs are an annoying hurdle to getting things done. And today, we want to show you a (not so seriously meant) way to deal with such bugs. We will build a self-healing program, which just handles your faulty code for you. On this journey, you will learn a few details about Unix signals and their concrete implementation in Linux. The signal(7) man page gives a good overview about the topic of signals.

We will handle two kinds of bugs that might arise in your programs:

  1. Invalid memory accesses that provoke a segmentation fault. This can happen, for example if you build a pointer out of thin air and dereference it. In this case, the memory management unit will catch this invalid memory access, generate a page fault, which the operating system forwards as a SIGSEGV signal to our process. The default action to take on SIGSEGV is to abort the program.

  2. Invalid jump that provokes an illegal instruction exception. If your program tries to execute bytes that do not look familiar to your CPU, you will get an illegal instruction trap. The trap is handled, like the page fault, by the operating system and leads by default to an abort of the process. The signal is SIGILL.

In a nutshell, we want to install two signal handlers with sigaction(2) to catch these errors and handle them accordingly. For the segmentation fault, we use mmap(2) to allocate an anonymous page-sized mapping at the accessed memory region. After returning from the handler, the process will repeat the previously invalid access, which will now succeed. For illegal instructions, we manipulate the process state, before returning, to jump over the invalid instruction.

Signals, in their original idea, are very close to hardware interrupts and traps. They can in principle happen everywhere in your program and interrupt the current control flow. So instead of fetching the next instruction from your currently running function, the operating system forces the thread to execute the previously defined signal handler. While the signal handler is running, the "main"-program is suspended. Therefore, we can say that a signal-handler execution is a function call that is forced upon the main program.

As there are multiple signals that the operating system can send to our process, the process has to inform the operating system, beforehand, which handler is connected to which signal. And we can do this with sigaction(2), which configures a signal with a struct sigaction:

      struct sigaction {
           void     (*sa_handler)(int);
           void     (*sa_sigaction)(int, siginfo_t *, void *);
           sigset_t   sa_mask;
           int        sa_flags;
           void     (*sa_restorer)(void);
       };

First, we see that there are two handler fields we can configure: sa_handler only takes the signal number, while sa_sigaction additionally gets more information about the signal with siginfo_t and about the interrupted program with ucontext_t (cast to void*). For our goal, we have to use sa_sigaction, which we select by setting the SA_SIGINFO flag.

On the kernel side, each process has a struct sighand_struct, which stores information about the configured signal handlers. The table is manipulated in do_sigaction. Afterwards, the signal is delivered on x86 in handle_signal.

Task

  1. Handle Segmentation faults by setting up a SIGSEGV handler that maps pages to info->si_addr.
  2. Handle illegal instructions by setting up a SIGILL handler that jumps 4 bytes forward and hopefully over the illegal instruction.
  3. Set up a SIGINT handler that sets the do_exit flag.