Chapter 21.2 — Other Ways to Terminate a Signal Handler

 

Chapter 21.2 — Other Ways to Terminate a Signal Handler
sigsetjmp · siglongjmp · abort() | EmbeddedPathashala
21.2
Section
3
Code Examples
8
Interview Qs

Key Terms
setjmp() longjmp() sigsetjmp() siglongjmp() sigjmp_buf savesigs abort() SIGABRT canJump guard Signal Mask

Ways to Exit a Signal Handler

Method Effect Use Case
Normal return Control returns to where it was interrupted Most common — flag pattern
_exit() Process terminates immediately Fatal errors; cleanup done
kill() / raise() Send a terminating signal to self Terminate with specific signal (e.g. SIGTERM)
siglongjmp() Jump to a saved point in main program Error recovery, shell command loop restart
abort() Terminates with core dump via SIGABRT Programming errors, assertions

21.2.1 — The Problem with longjmp() from a Signal Handler

When a signal handler is entered, the kernel automatically adds the signal to the process signal mask (blocks it). When the handler returns normally, the kernel removes it from the mask.

But if you use longjmp() to exit the handler (jumping back to main), the behaviour of the signal mask depends on the UNIX variant:

System V / Linux behaviour

longjmp() does NOT restore the signal mask. The signal that caused the handler invocation stays blocked after the jump. On Linux, Ctrl+C would be permanently blocked!

BSD behaviour

setjmp() saves the signal mask in its env argument. longjmp() restores it. Signal is unblocked after jump. BSD also provides _setjmp()/_longjmp() with System V semantics.

Because of this portability problem, POSIX.1-1990 introduced sigsetjmp() and siglongjmp() which give explicit control over signal mask handling.

sigsetjmp() and siglongjmp() — The Portable Solution
#include <setjmp.h>

/*
 * sigsetjmp() - saves execution context + optionally the signal mask
 *
 * env:      buffer to save state into (type: sigjmp_buf, not jmp_buf)
 * savesigs: if nonzero, the CURRENT signal mask is saved in env
 *           and will be restored by siglongjmp()
 *
 * Returns 0 when called directly.
 * Returns the val argument of siglongjmp() when returning via jump.
 */
int sigsetjmp(sigjmp_buf env, int savesigs);

/*
 * siglongjmp() - jumps back to the sigsetjmp() point
 *
 * env: must be the same env buffer previously filled by sigsetjmp()
 * val: value sigsetjmp() will appear to return (should be non-zero)
 *
 * If savesigs was nonzero in sigsetjmp(), the signal mask saved at
 * that time is RESTORED — i.e., the signal that blocked the handler
 * entry is unblocked.
 */
void siglongjmp(sigjmp_buf env, int val);

Execution Flow with sigsetjmp/siglongjmp
main(): sigsetjmp(env, 1) → returns 0 → set canJump=1 → enter pause() loop
↓ Signal arrives
handler(): signal mask has SIGINT added. Check canJump. Call siglongjmp(env, 1)
↓ Jump executes
main(): sigsetjmp() appears to return 1 → signal mask RESTORED (SIGINT unblocked)
main(): printSigMask() shows empty mask. Back in main program. Handler done.

Code Example 1 — sigsetjmp + siglongjmp with Guard Variable
#define _GNU_SOURCE
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

static sigjmp_buf env;

/*
 * Guard variable: canJump = 0 until sigsetjmp() has been called.
 * Without this, a signal arriving BEFORE sigsetjmp() would try
 * to jump using an uninitialized env buffer — undefined behaviour.
 */
static volatile sig_atomic_t canJump = 0;

static void handler(int sig)
{
    /* UNSAFE: printf is not async-signal-safe, used here for demo only */
    printf("[Handler] Signal %d received. Signal mask now has this signal blocked.\n", sig);

    if (!canJump) {
        /* env not yet set — safe fallback: just return */
        printf("[Handler] env not initialized. Returning normally.\n");
        return;
    }

    /* siglongjmp with savesigs=1 restores signal mask (unblocks SIGINT) */
    siglongjmp(env, 1);
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    /*
     * sigsetjmp(env, 1):
     *   - First call: saves context + signal mask. Returns 0.
     *   - After siglongjmp(): returns 1. Signal mask is RESTORED.
     */
    if (sigsetjmp(env, 1) == 0) {
        /* First time: just set up the guard */
        canJump = 1;
        printf("[Main] sigsetjmp() done. Press Ctrl+C to jump back here.\n");
    } else {
        /* Returned via siglongjmp() */
        printf("[Main] Returned from siglongjmp(). Signal mask restored.\n");
        printf("[Main] SIGINT is now UNBLOCKED again. Press Ctrl+C to trigger again.\n");
    }

    /* Main loop — wait for signals */
    int count = 0;
    while (count < 3) {
        pause();
        count++;
    }

    printf("[Main] Received %d signals via nonlocal goto. Exiting.\n", count);
    return 0;
}
Key Insight: The canJump guard variable prevents the handler from calling siglongjmp() before sigsetjmp() has initialized the env buffer. Without this guard, a very early signal would cause undefined behaviour.

Code Example 2 — Demonstrating Signal Mask NOT Restored with longjmp()
#define _GNU_SOURCE
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <unistd.h>

/*
 * Demonstrates the Linux/System V behaviour:
 * longjmp() does NOT restore the signal mask.
 * After the jump, SIGINT remains blocked — Ctrl+C stops working!
 *
 * To fix: replace setjmp/longjmp with sigsetjmp/siglongjmp (savesigs=1).
 */

static jmp_buf env;
static volatile sig_atomic_t canJump = 0;

static void handler(int sig)
{
    printf("[Handler] SIGINT received. About to longjmp...\n");
    if (!canJump) return;
    longjmp(env, 1);   /* Signal mask is NOT restored here on Linux */
}

void print_sigint_status(void)
{
    sigset_t pending;
    sigprocmask(SIG_BLOCK, NULL, &pending); /* query current mask */
    if (sigismember(&pending, SIGINT))
        printf("[Mask Check] SIGINT is BLOCKED\n");
    else
        printf("[Mask Check] SIGINT is unblocked\n");
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    printf("Before sigsetjmp:\n");
    print_sigint_status();

    if (setjmp(env) == 0) {
        canJump = 1;
    } else {
        printf("After longjmp():\n");
        print_sigint_status();
        /* On Linux: SIGINT is STILL BLOCKED here! */
        printf("Try Ctrl+C now — it is blocked and has no effect.\n");
    }

    while(1) pause();
    return 0;
}
On Linux: After longjmp() from the handler, SIGINT stays blocked. Further Ctrl+C presses have no effect. Replace with sigsetjmp(env, 1) + siglongjmp() to restore the mask.

21.2.2 — abort(): Abnormal Process Termination

abort() terminates the process by raising SIGABRT, which by default generates a core dump. This is useful for catching programming errors (like a failed assertion).

Property abort() exit() _exit()
Flushes stdio Yes (if terminates) Yes No
Core dump Yes (default) No No
Signal raised SIGABRT None None
Can be caught Yes (but abort() ensures death) N/A N/A
Safe from handler Yes No Yes
abort() guarantees termination: Even if a SIGABRT handler is installed and returns, abort() re-raises SIGABRT with the default action (SIG_DFL) to guarantee the process dies. The only way to survive abort() is a SIGABRT handler that calls siglongjmp() to escape.

Code Example 3 — Using abort() for Defensive Programming
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

/*
 * Custom error handler that calls abort().
 * The core dump allows a debugger to inspect state at time of failure.
 */
void fatal_error(const char *msg)
{
    fprintf(stderr, "FATAL: %s\n", msg);
    fprintf(stderr, "Calling abort() — a core dump will be generated.\n");
    abort();   /* Raises SIGABRT → core dump + termination */
}

/*
 * A SIGABRT handler that runs before the process dies.
 * It can do cleanup, but abort() will FORCE termination afterward.
 */
static void sigabrt_handler(int sig)
{
    /* write() is async-signal-safe */
    const char msg[] = "[SIGABRT Handler] Caught SIGABRT. Doing last-chance cleanup.\n";
    write(2, msg, sizeof(msg) - 1);
    /* If we return from this handler, abort() re-raises SIGABRT with SIG_DFL */
}

int main(void)
{
    /* Install a SIGABRT handler to demonstrate last-chance cleanup */
    struct sigaction sa;
    sa.sa_handler = sigabrt_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGABRT, &sa, NULL);

    /* Simulate a program that detects an internal inconsistency */
    int *ptr = NULL;

    /* Defensive check — in production this might be an assert() */
    if (ptr == NULL) {
        fatal_error("Null pointer dereference detected before use");
    }

    *ptr = 42;  /* Would crash without the check above */
    return 0;
}
Compile: gcc -Wall -o abort_demo abort_demo.c
Run: ./abort_demo — The SIGABRT handler fires, then the process terminates with a core dump.
Inspect core: ulimit -c unlimited && ./abort_demo && gdb ./abort_demo core

Interview Questions — Nonlocal Goto & abort()
Q1. What is the problem with using longjmp() to exit a signal handler on Linux?
On Linux (System V behaviour), longjmp() does NOT restore the process signal mask. When a signal handler is entered, the kernel adds that signal to the mask (blocks it). If you exit via longjmp(), the signal remains blocked in the mask permanently — e.g., after jumping out of a SIGINT handler, Ctrl+C stops working completely.
Q2. What is sigsetjmp() and how does it differ from setjmp()?
sigsetjmp(env, savesigs) is like setjmp() but with an extra savesigs argument. If savesigs is nonzero, the current process signal mask is saved into env (which is of type sigjmp_buf, not jmp_buf). When siglongjmp() jumps back, the saved signal mask is restored — unblocking any signals that were blocked on handler entry.
Q3. What is the canJump guard variable and why is it essential?
If a signal arrives before sigsetjmp() is called, the env buffer is uninitialized. A siglongjmp() using an uninitialized buffer causes undefined behaviour (likely crash). The canJump guard is set to 1 only after sigsetjmp() completes successfully. The handler checks canJump before calling siglongjmp(), and simply returns if not yet set.
Q4. What does abort() do internally?
abort() raises SIGABRT. The default action of SIGABRT is to terminate the process and generate a core dump. SUSv3 requires that abort() override any blocking of SIGABRT. If a handler catches SIGABRT and returns, abort() resets the disposition to SIG_DFL and raises SIGABRT again, guaranteeing process termination.
Q5. Why can’t you use exit() inside a signal handler? What should you use instead?
exit() is not async-signal-safe because it flushes and closes stdio buffers (calls into the non-reentrant stdio library). If the signal interrupted the main program while inside a stdio function, calling exit() from the handler would cause double-entry into the stdio machinery, corrupting state. Use _exit() instead, which immediately syscalls the kernel to terminate without touching userspace buffers.
Q6. What is the difference between sigjmp_buf and jmp_buf?
jmp_buf stores only the CPU register state (program counter, stack pointer, callee-saved registers). sigjmp_buf stores the same registers AND, when savesigs=1, also stores the process signal mask. Using the wrong type with the wrong function is undefined behaviour.
Q7. When would you use siglongjmp() in a real program?
The classic use case is a shell: when the user presses Ctrl+C during command execution, the SIGINT handler calls siglongjmp() back to the main command-reading loop, abandoning the current command cleanly without exiting the shell itself. Another use: implementing a timeout on a blocking operation by recovering from a SIGALRM delivery.
Q8. Can a SIGABRT handler prevent abort() from killing the process?
Only if the handler calls siglongjmp() to escape — this causes abort()‘s second SIGABRT raise to never execute. If the handler simply returns, abort() resets the action to SIG_DFL and raises SIGABRT again, killing the process unconditionally. So a normal return from the handler does NOT prevent death.

Next: Handling Signals on an Alternate Stack →

Chapter 21.3 — sigaltstack() ← Previous

Leave a Reply

Your email address will not be published. Required fields are marked *