Sending and Intercepting a Signal in C

Table of Contents

Anyone who’s ever been confronted to segfaults and bus error will already be familiar with the idea of signals. We get a SIGSEGV or a SIGBUS which ends our program’s execution without notice, without explanation and without appeal. But what really is a signal? Is it more than just the operating system’s police baton? And how can we send, block and even intercept one within our programs? That’s what we will discover in this article.

What is a Signal?

A signal is a standardized notification message used in Unix and POSIX-compliant operating systems. It is asynchronously sent to a running program to notify it of some event. The system interrupts the process’ normal execution to trigger a specific reaction like, among other things, terminating it. So signals are a sort of inter-process communication.

Let’s take a global look at how such a notification message is transferred to a destination process.

How a Signal is Sent

The operating system’s kernel can send a signal for one of the two following reasons:

  • it has detected a system-wide event like a divide-by-zero error or the end of a child process,
  • a process requested a signal to be sent with the kill system call (a process can send itself a signal in this way).

In reality, “sending” a signal is more like delivering it: the system updates the destination process’ context. Indeed, for each process, the kernel maintains two bit vectors: pending, to monitor pending signals, and blocked, to keep track of the blocked signals. When it sends a signal, the kernel simply sets the appropriate bit to 1 in the destination process’ pending bit vector.

Example of the pending and blocked bit vectors of a process. The pending vector keeps track of pending signals. A process can also indicate a signal to be added to the blocked bit vector.
Example of 32-bit pending and blocked vectors in a process. Here, signal 17 ( SIGCHLD) was sent, so it is pending. This same process also has previously set signal 2 ( SIGINT) as blocked.

It’s important to realize that there can only ever be a single pending signal of any particular type. In the image above, the process already has a pending signal 17, SIGCHLD. This means that the system cannot send it any other SIGCHLD signals until this one is received. There is no queue for pending signals: as long as this signal has not been received, all other signals of the same type that should’ve been sent are discarded instead.

How a Signal is Received

The operating system seems to be able to multitask, but this is only an illusion. In truth, it is constantly switching from one process to the next at lightning speed. This is called “context switching”.

When the kernel resumes the execution of a process, for example after one of these context switches or after completing a system call, it checks the unblocked pending signal set. This is done with the bitwise operation pending & ~blocked. If that set is empty, as is usually the case, it moves on to the program’s next instruction. However, if the set is not empty, the kernel chooses a signal (typically the smallest) and forces the process to react to it with an action. This is the moment we call “receiving” the signal. Depending on is type, the process will either:

  • ignore the signal,
  • terminate its own execution,
  • intercept the signal by executing its own handler in response.

Once the signal received and one of these actions performed, the kernel resets the corresponding bit in the pending bit vector and moves on to the following instruction if the program has not yet terminated.

POSIX Signals and Their Default Actions

So what are all of these signals that the system can send to processes? The list below shows all of the signals available on Linux. Other POSIX-compatible operating systems will also have these, but they may have different associated numeric values. We can consult the list of available signals and their corresponding numbers with the following command:

kill -l

By default, a process receiving a signal will perform one of these four actions:

  • Terminate: the process immediately terminates,
  • Core: the process immediately terminates does a core dump (creates a file containing a copy of its memory and registers for future analysis),
  • Ignore: the signal is simply ignored and the program carries on with its regular execution,
  • Stop: suspends the process’ execution until it receives the SIGCONT signal.

Later, we will see how to change the default action associated with a signal. However, it is impossible to intercept, ignore, block or change the action of the SIGKILL and SIGSTOP signals.

List of Signals on Linux

Number Name Default Action Description
1 SIGHUP Terminate Terminal line hangup or parent process terminated
2 SIGINT Terminate Interrupt from keyboard ( ctrl-c)
3 SIGQUIT Terminate Quit from keyboard ( ctrl-\)
4 SIGILL Terminate Illegal instruction
5 SIGTRAP Core Trace trap
6 SIGABRT Core Signal from abort function
7 SIGBUS Terminate Bus error
8 SIGFPE Core Floating-point exception
9 SIGKILL Terminate Kill program
10 SIGUSR1 Terminate User-defined signal 1
11 SIGSEGV Core Invalid memory reference (segfault)
12 SIGUSR2 Terminate User-defined signal 2
13 SIGPIPE Terminate Write in pipe with no reader
14 SIGALRM Terminate Timer signal from alarm function
15 SIGTERM Terminate Software termination signal
16 SIGSTKFLT Terminate Stack fault on coprocessor
17 SIGCHLD Ignore Child process has stopped or terminated
18 SIGCONT Ignore Continue process if stopped
19 SIGSTOP Stop Stop signal not from terminal
20 SIGTSTP Stop Stop signal from terminal ( ctrl-z)
21 SIGTTIN Stop Background process read from terminal
22 SIGTTOU Stop Background process wrote to terminal
23 SIGURG Ignore Urgent condition on socket
24 SIGXCPU Terminate CPU time limit exceeded
25 SIGXFSZ Terminate File size limit exceeded
26 SIGVTALRM Terminate Virtual timer expired
27 SIGPROF Terminate Profiling timer expired
28 SIGWINCH Ignore Window size changed
29 SIGIO Terminate I/O now possible on a descriptor
30 SIGPWR Terminate Power failure
31 SIGSYS Terminate Bad system call

Sending a Signal

In Unix-type systems, there are several mechanisms to send signals to processes. All of them use the concept of process groups.

Each process belongs to a group which is identified by a positive integer, the PGID ( p rocess g roup id entifier). It’s an easy way to recognize related processes. By default, all child processes belong to their parent’s process group. This allows the system to send a single signal to all of the processes within a group at once.

However, a process can change its own group or the group of another process. This is what our shell does when it creates child processes to execute the user’s input. Let’s display the process identifier ( PID), the parent process identifier ( PPID) and the process group identifier ( PGID) of all the processes associated with our shell with the ps command:

ps -eo "%c: [PID = %p] [PPID = %P] [PGID = %r]" | grep $$

Output of the ps command that shows a process PID, PPID and PGID.

Here, we can see that le shell, Bash, uses its own PID as its group identifier, not its parent’s. It also creates a new group identifier (5213) for its child processes, ps and grep (5213 and 5214 respectively). This allows Bash to send a single signal to all of its children at once, if need be. Of course, in this example, the child processes’ are very short-lived. But if that wasn’t the case, how would we send them a signal?

Sending a Signal From the Keyboard

From the shell in our terminal, there are three keyboard shortcuts that allow us to interrupt all of the running foreground processes:

  • ctrl-c: sends SIGINT to interrupt them,
  • ctrl-\: sends SIGQUIT to kill them,
  • ctrl-z: sends SIGTSTP to suspend them.

Of course, these shortcuts do not affect background processes. But what if we want to send one of the 28 other signals instead?

Sending Signals with the Kill Command

To send another type of signal from our terminal, we will need to use the kill command, even for a signal that does not terminate a process! Some shells have their own builtin kill command. Here, we will only be looking at the program found in /bin/kill.

First, we must find the PID or PGID of the process or processes to send our signal to. To display the PID of a process, we can use the pidof, pgrep, top, or, as we saw earlier, ps commands.

Let’s say we want to send the termination signal SIGKILL (number 9) to the process with the PID 4242. We can do this in one of three ways with the kill command:

/bin/kill -9 4242
/bin/kill -KILL 4242
/bin/kill -SIGKILL 4242

Now, let’s say that the 4242 process has several children that belong to the 4242 group. How do we indicate that we mean the group 4242, not the process? For the kill command, a negative number means a PGID and not a PID. So in order to kill all of the processes in the 4242 group, we can:

/bin/kill -9 -4242
/bin/kill -KILL -4242
/bin/kill -SIGKILL -4242

To signal all of the processes in the current group, we can put 0 as a PID. To send a signal to all of the system’s processes except for kill itself and init ( PID 1), we can indicate -1 as a PID.

The /bin/kill command also allows us to display a list of all signals with its -L option. And with its -l option, we can search for a signal by number ( /bin/kill -l 11) or by name ( /bin/kill -l SIGSEGV or /bin/kill -l SEGV).

Sending a Signal with the Kill System Call in C

In a previous article about creating and killing child processes, we quickly went over the kill system call from the <signal.h> library, There are several other system calls to request a signal to be sent from our C programs, but this one is the most commonly used. Let’s recall its prototype:

int kill(pid_t pid, int sig);

This system call works in much the same way as the /bin/kill command described above. Its parameters are:

  • pid: the identifier of the process or process group to send the signal to. Here, we can specify:
    • a positive integer: a process’ PID,
    • a negative integer: a process group’s PGID,
    • 0: all of the processes in the calling process’ group,
    • -1: all of the processes in the system (except process 1, init!) for which the calling process has the permission to send signals to. See the manual page for kill (2) about permissions.
  • sig: the signal to send to the process.

The kill function returns 0 for success and -1 for failure, in which case it sets errno to indicate the error details.

Intercepting a Signal and Changing its Default Action in C

We saw the default actions associated with each signal in the table above. For example, by default, SIGKILL triggers the process’ immediate termination, whereas the action associated with SIGCHLD is to ignore it completely. However, we are not restricted to those default behaviors: a process can change the action with which to react thanks to one or more handlers. The only exceptions to this are SIGKILL and SIGSTOP, which cannot be intercepted or have their default actions modified.

Intercepting a Signal with Sigaction

The <signal.h> library offers two functions to intercept a signal: signal and sigaction. Since the first is not recommended because of portability issues, let’s take a look at sigaction, whose prototype is:

int sigaction(int signum, const struct sigaction *restrict act,
              struct sigaction *restrict oldact);

This prototype is slightly intimidating, so let’s explain its obscure parameters:

  • signum: the signal we want to modify the action of,
  • act: a pointer to a sigaction-type structure that will allow us, among other things, to indicate a signal handler. We will examine this shortly.
  • oldact: a pointer to another sigaction-type structure in which to store the old behavior that was triggered upon receipt of the signal. If we don’t need to restore the old action later, then we can just put NULL here.

In case of success, sigaction returns 0. In case of error, it returns -1 and sets errno.

Providing a Signal Handler to the Sigaction Structure

Clearly, “sigaction” is the name of the function as well as of the structure type that the function needs to do its job. Let’s glance at this structure in the manual page for sigaction. The interesting variable here is sa_handler. This is what specifies the action associated with the signal. We can indicate one of three things, here:

  • SIG_DFL for the default action,
  • SIG_IGN to ignore the signal,
  • a pointer to a signal handler, which is a function that will trigger as a response to this signal. It must have the following prototype: void function_name(int signal);. As we can see, this function will take a signal as a parameter, which means we can use this same function to handle several different functions!

The sigaction structure has another variable, sa_flags, that offers different options to modify the signal action, but we will not linger on it in this article. The sa_mask variable allows us to block specific signals during the handler’s execution. We will revisit this in the section on blocking signals.

Let’s also keep in mind that another variable in the structure ( sa_sigaction) is incompatible with sa_handler: we can’t specify both. Therefore, we should be careful and make sure all of the bits in the structure are initialized to 0 with a function like bzero or memset before filling it out.

Sigaction in Action

Let’s test all of this with a simple program that will only print a message each time we press ctrl-c, meaning each time we send the SIGINT signal:

#include <signal.h>
#include <stdio.h>
#include <strings.h>

// Signal handler for SIGINT
void sigint_handler(int signal)
{
 if (signal == SIGINT)
  printf("\nIntercepted SIGINT!\n");
}

void set_signal_action(void)
{
 // Declare the sigaction structure
 struct sigaction act;

 // Set all of the structure's bits to 0 to avoid errors
 // relating to uninitialized variables...
 bzero(&act, sizeof(act));
 // Set the signal handler as the default action
 act.sa_handler = &sigint_handler;
 // Apply the action in the structure to the
 // SIGINT signal (ctrl-c)
 sigaction(SIGINT, &act, NULL);
}

int main(void)
{
 // Change SIGINT's associated action
 set_signal_action();
 // Infinite loop to give us time to do ctrl-c
 // as much as we want!
 while(1)
  continue ;
 return (0);
}

Output:

Output of a test program in C to show how to intercept a signal and change its default action.

The message is printed whenever we press ctrl-c, which means that the signal was correctly intercepted and triggers our handler. We can now quit the process with ctrl-\ ( SIGQUIT).

Securing Signal Handlers

Signals are asynchronous, which means that they can occur at any time during our program’s execution. When we intercept them with a handler, we never know where the program is in its execution. If the handler accesses a variable that the program is currently using, the results could be disastrous. And, we can’t forget that a handler can itself be interrupted by another handler (or itself!) if the process receives another signal in the meantime!

Signal handlers are a form of concurrent programming. Yet, as we’ve previously seen in the article about threads and mutexes, concurrent programming can cause unforeseeable and extremely difficult errors to debug. To avoid these kinds of errors, we should take a lot of care when formulating handlers, so that they are as safe as possible. In light of this, here are a few recommendations to keep in mind.


1. Keep signal handlers as simple and short as possible

Sometimes, all a handler has to do is set a flag and let the main program take care of processing the signal. The program will just have to periodically check the flag to know if any response is required.


2. Only use async-signal-safe functions in handlers

The signal (7) manual page maintains a list of safe functions to use in signal handlers. Let’s take note that most of the popular functions like printf and even exit are not on this list! This also means that we might need to reconsider our example in the previous section…


3. Save and restore errno

Most of the safe functions mentioned above update errno when they fail. The handler risks interfering with other parts of the program relying on errno. To avoid this, the handler can save a copy of errno at the beginning of its execution in order to restore it at the end.


4. Temporarily block all signals when accessing data shared between the main program and the handler

Reading from and writing to a variable or any data structure requires a sequences of behind-the-scenes instructions. If the handler interrupts the main program and accesses the same variable at the same time, it might find that data in an inconsistent state. Blocking signals while accessing the data guarantees that the handler cannot interrupt a previous access. We will discover how to block signals in the next section.


5. Declare shared global variables as volatile

If the handler updates a global variable that the main program reads from time to time, the compiler might think that the main program never updates the variable. For optimization’s sake, the compiler might choose to cache it, which means that the main program might never see the updated value. The keyword volatile tells to the compiler never to cache the variable.


6. Declare a flag with sig_atomic_t type

If the signal handler updates a flag that the main program periodically checks to react to the signal before clearing, we can use the sig_atomic_t type. For this integer type, reading and writing is atomic, meaning they are uninterruptible. This way, we don’t need to temporarily block all signals while accessing the flag.


Blocking a Signal in C

In order to block a signal, we must first add it to a set of signals to be blocked. Then, we will have to give this set either to the sa_mask variable of the sigaction structure that we will give to the sigaction function, or to a dedicated function, sigprocmask. The <signal.h> library once again provides us will the variable types and the functions we need.

Before going any further, let’s remember that it is pointless to attempt to block the SIGKILL or SIGSTOP signal!

Creating a Set of Signals to be Blocked

First of all, we need a variable of type sigset_t to store the set of signals we wish to block.

sigset_t    signal_set;

Then, we need to initialize it with one of the following functions:

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

As its name suggests, sigemptyset sets all of the set’s bits to 0, which means there are no signals in the set. Inversely, the sigfillset function sets all of the set’s bits to 1, meaning it adds all signals to the set. Both of these functions return 0 on success or -1 on failure.

Then, we can add or remove a signal from the set with these two functions:

int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

The sigaddset function adds the signum signal to the set while sigdelset removes the signum signal from the set. Both of these functions also return 0 for success and -1 for failure.

To determine if a signal is in a set or not, we can use the following function:

int sigismember(const sigset_t *set, int signum);

If the signum signal we give it is part of the provided set, the sigismember function returns 1, if it is not, it returns 0. If an error occurred, it returns -1.

Blocking the Signals in the Set

Once we have our set of signals to block, we can use either sigaction or sigprocmask to block them.

Let’s note that sigaction only allows us to block the signals in the set for the duration of the signal handler’s execution. For this, we must specify the set in sigaction’s sa_mask variable as well as a signal handler in its sa_handler variable.

The sigprocmask function allows us to block and unblock a signal at any time. It’s prototype is:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

Its parameters are:

  • how: an integer representing the operation to perform with the given set. We can specify:
    • SIG_BLOCK to add the set’s signals to the process’ blocked bit vector and block them. This represents the bitwise blocked = blocked | set operation.
    • SIG_UNBLOCK to remove the set’s signals from the process’ blocked bit vector to unblock them. This represents the bitwise blocked = blocked & ~set operation.
    • SIG_SETMASK to replace the blocked bit vector with the given set. Simply represents blocked = set.
  • set: a pointer to the set or signals that should be added/removed or that should replace the blocked bit vector
  • oldset: a pointer to a memory area to store the old blocked bit vector that will be replaced. If we don’t need to restore the old signals that were blocked, we can indicate NULL here.

Signal Blocking Example

Let’s modify our previous test example. We will still intercept SIGINT ( ctrl-c on the keyboard), but this time, with a secure signal handler. Now, the handler will only set a global flag that the main program will regularly check. To protect this global variable, we will block the SIGINT signal whenever we read or write to it. At the beginning of the program, we will immediately block SIGQUIT ( ctrl-\). It can only be unblocked following a SIGINT signal.

#include <signal.h>
#include <strings.h>
#include <stdio.h>
#include <unistd.h>

// Global variable, shared between the main program and
// the SIGINT handler. The handler will set this variable to 1
// when we press ctrl-c.
// Declared volatile to avoid caching due to compiler optimization
volatile int g_unblock_sigquit = 0;

// Blocks the specified signal
void block_signal(int signal)
{
// Set of signals to block
 sigset_t sigset;

// Initialize set to 0
 sigemptyset(&sigset);
// Add the signal to the set
 sigaddset(&sigset, signal);
// Add the signals in the set to the process' blocked signals
 sigprocmask(SIG_BLOCK, &sigset, NULL);
 if (signal == SIGQUIT)
  printf("\e[36mSIGQUIT (ctrl-\\) blocked.\e[0m\n");
}

// Unblocks the given signal
void unblock_signal(int signal)
{
// Set of signals to unblock
 sigset_t sigset;

// Initialize the set to 0
 sigemptyset(&sigset);
// Add the signal to the set
 sigaddset(&sigset, signal);
// Remove set signals from the process' blocked signals
 sigprocmask(SIG_UNBLOCK, &sigset, NULL);
 if (signal == SIGQUIT)
  printf("\e[36mSIGQUIT (ctrl-\\) unblocked.\e[0m\n");
}

// SIGINT signal handler
void sigint_handler(int signal)
{
 if (signal != SIGINT)
  return ;
// Blocks other SIGINT signals to protect the global
// variable during access
 block_signal(SIGINT);
 g_unblock_sigquit = 1;
 unblock_signal(SIGINT);
}

void set_signal_action(void)
{
// Declare sigaction structure
 struct sigaction act;

// Initialize structure to 0.
 bzero(&act, sizeof(act));
// Add new signal handler
 act.sa_handler = &sigint_handler;
// Apply new signal handler to SIGINT (ctrl-c)
 sigaction(SIGINT, &act, NULL);
}

int main(void)
{
// Change the default action for SIGINT (ctrl-c)
 set_signal_action();
// Block the SIGQUIT signal (ctrl-\)
 block_signal(SIGQUIT);
// Infinite loop to give us time to do ctrl-\ and ctrl-c
// as many times as we wish
 while(1)
 {
//  Block the SIGINT signal while global variable is read
  block_signal(SIGINT);
//  If SIGINT signal handler set the global variable
  if (g_unblock_sigquit == 1)
  {
//   SIGINT (ctrl-c) was received.
   printf("\n\e[36mSIGINT detected. Unblocking SIGQUIT\e[0m\n");
//   Unblock SIGINT and SIGQUIT
   unblock_signal(SIGINT);
   unblock_signal(SIGQUIT);
  }
//  Otherwise, unblock SIGINT and keep looping
  else
   unblock_signal(SIGINT);
  sleep(1);
 }
 return (0);
}

Output of a test program in C to show how to intercept and block a signal.

In this output, we can see that when we press ctrl-\, meaning when we send the SIGQUIT signal, nothing happens (apart from the terminal displaying ^\). So the signal is indeed blocked by our process. The instant we press on ctrl-c to send the SIGINT signal, the SIGQUIT signal gets unblocked.

But ad we can see here, we don’t even have time to do another ctrl-\ to actually send SIGQUIT again. We can’t even see our line 41 printf that confirms SIGQUIT’s unblocking! This is because the SIGQUIT signal was pending ever since our first ctrl-\. When it was unblocked, its default action (“Quit (code dumped)”) was triggered.


A little tip to share, a nagging question to ask, or a strange discovery to discuss about signals and their handlers? I’d love to read and respond to it all in the comments. Happy coding !

Sources and Further Reading

  • Bryant, R. E., O’Hallaron, D. R., 2016, Computer Systems: A Programmer’s Perspective, Third Edition, Chapter 8 : Exceptional Control Flow, pp. 792-817

  • Linux Programmer’s Manual:

  • Stack Exchange, Linux Process Group Example [stackexchange.com]

Comments

Related Posts

CTF Walkthrough: Wonderland on TryHackMe

Wonderland is a freely-available capture the flag (CTF) challenge created by NinjaJc01 on TryHackMe.

Read More

Local, Global and Static Variables in C

A variable is a name we give to a memory storage area that our program can then manipulate.

Read More