On this page

Signals and ptrace

Coverage: Signal lifecycle (generation → delivery → handling) → signal_struct/sighand_struct → reliable/irreliable signals → ptrace mechanism (PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA) → seccomp mutual exclusion Kernel versions: 2.6 ~ 6.x

Overview

Signals are one of the oldest IPC mechanisms in Unix, and also serve as a unified exception/event notification framework. Whether it is SIGINT generated by pressing Ctrl-C, SIGFPE triggered by division by zero, or SIGCHLD when a child process exits, they all follow the same kernel path.

ptrace is the foundational system call for debuggers (gdb, strace) and sandboxes (seccomp user notification), allowing one process to observe and control the execution of another process.


Signal Data Structures

Signal Set (sigset_t)

// include/linux/signal_types.h
// A signal set is a bitmap, where each bit represents a signal (1~64)
typedef struct {
    unsigned long sig[_NSIG_WORDS];  // _NSIG=64, _NSIG_WORDS=2 (on 64-bit)
} sigset_t;

// Common operations
sigemptyset(&set);           // Clear
sigaddset(&set, SIGINT);     // Add
sigdelset(&set, SIGTERM);    // Remove
sigismember(&set, SIGKILL);  // Test

Per-process Signal Structures

// include/linux/sched.h
struct task_struct {
    struct signal_struct    *signal;   // Shared by thread group
    struct sighand_struct   *sighand;  // Shared by thread group (unless CLONE_SIGHAND is used)
    sigset_t                blocked;   // Per-thread blocked signal set
    sigset_t                real_blocked;
    struct sigpending       pending;   // Per-thread pending signals
};

// include/linux/sched/signal.h
struct signal_struct {
    // Thread group shared state
    int                     nr_threads;
    struct sigpending       shared_pending;  // Signals sent to all threads in the group
    // fork/exec statistics
    // Resource limits
};

struct sigpending {
    struct list_head        list;       // List of sigqueue for pending signals
    sigset_t                signal;     // Bitmap for fast lookup
};

// include/linux/sched/signal.h
struct sighand_struct {
    refcount_t              count;      // Reference count
    spinlock_t              siglock;    // Protects the signal action table
    wait_queue_head_t       signalfd_wqh;
    struct k_sigaction      action[_NSIG];  // Action for each signal
};

sigaction — Signal Handling Action

// include/linux/signal_types.h
struct k_sigaction {
    struct sigaction sa;  // Contains sa_handler / sa_sigaction / sa_mask / sa_flags
};

// Key bits in sa_flags:
//   SA_NOCLDSTOP: Do not receive SIGCHLD when child stops
//   SA_NOCLDWAIT: Automatically reap child on exit, do not create zombies
//   SA_SIGINFO:   Use three-argument handler (sig, siginfo, context)
//   SA_RESTART:   Automatically restart syscall interrupted by signal
//   SA_NODEFER:   Do not automatically block the signal while handling it
//   SA_RESETHAND: Reset to SIG_DFL after handling once (one-shot)

Signal Lifecycle

Generate → Pending → Deliver → Handle

1. Generation: send_signal()

// kernel/signal.c
int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
                enum pid_type type) {
    struct sigpending *pending;

    if (type == PIDTYPE_PID)
        pending = &t->pending;              // Sent to specific thread
    else
        pending = &t->signal->shared_pending;  // Sent to entire thread group

    // Check if the same signal is already pending (deduplication for unreliable signals)
    if (legacy_queue(pending, sig))
        goto ret;  // Unreliable signal: only set bit in bitmap, do not queue

    // Reliable signals (SIGRTMIN ~ SIGRTMAX): allocate sigqueue for queuing
    //   → Even for the same signal type, each send is queued
    //   → No loss of signals
    __sigqueue_alloc(sig, t, GFP_ATOMIC, ...);

    // Set pending bitmap → checked when returning to user space
    sigaddset(&pending->signal, sig);

    // Set TIF_SIGPENDING → trigger signal handling before returning to user space
    set_tsk_thread_flag(t, TIF_SIGPENDING);
}

2. Unreliable Signals vs. Reliable Signals

Traditional signals (1~31, "unreliable"):
  - Multiple sends of the same signal → pending bitmap records only 1 instance
  - If the kernel finds the bit is already set → new sends are discarded
  - Classic issue: Multiple child processes exit simultaneously → only one notification received

Real-time signals (SIGRTMIN=34 ~ SIGRTMAX=64, "reliable"):
  - Each send allocates a sigqueue for queuing
  - Can carry extra data (siginfo.si_value)
  - Signals are delivered in order of number (lower number first)
  - FIFO for signals of the same number

3. Delivery: get_signal() → handle_signal()

// arch/x86/kernel/signal.c
// Called before returning to user space (exit_to_user_mode_loop):
bool get_signal(struct ksignal *ksig) {
    // 1. Pick an unblocked signal from pending
    //    Check thread pending first → then shared_pending
    //    Signals are picked in ascending order of number
    //
    // 2. Handle ptrace interception (if any)
    //    If in ptrace-stop → check if consumed by ptracer
    //
    // 3. Handle cgroup signals (if any)
    //
    // 4. Handle SIGKILL / SIGSTOP (cannot be blocked or overridden by handler)
    //
    // 5. Check sa_handler:
    //    - SIG_IGN → Discard directly
    //    - SIG_DFL → Default action (core/stop/ignore/terminate)
    //    - Custom handler → Set up user-space stack frame, return handler address
    //
    // 6. Set ksig->ka (sigaction) and ksig->info (siginfo)
    return true;
}

// If handler != SIG_DFL/SIG_IGN:
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
    // 1. Create a sigframe on the user-space stack
    //    → Save current registers: RIP, RSP, RFLAGS, general-purpose registers
    //    → Save siginfo (si_signo, si_code, si_addr, etc.)
    //    → Save FPU state (XSAVE)
    //
    // 2. Modify regs:
    //    regs->rip = sa_handler    → Jump to signal handler upon returning to user space
    //    regs->rsp = sigframe      → Stack is above the signal frame
    //
    // 3. Return to user space → Process starts executing the signal handler
}

4. Signal Return: sigreturn()

// After the signal handler finishes, libc calls sigreturn()
// → sys_rt_sigreturn() → restore_sigcontext()
//   Restore all saved registers from the sigframe
//   → Process continues execution from where it was interrupted by the signal

The sigframe is the essence of the signal mechanism—the kernel fakes a "snapshot" on the user stack. After the handler runs, sigreturn restores the snapshot, and the process continues as if nothing happened (except for the side effects of the handler).

5. Signals and System Calls

Three ways to handle system calls interrupted by signals:
  SA_RESTART:   Kernel automatically retries (only for "slow" syscalls like read/write/wait/...)
  No SA_RESTART: Return -EINTR, user space decides whether to retry
  Fatal signal: Process exits, does not return
  
  ERESTARTSYS / ERESTARTNOINTR / ... :
    Special error codes used internally by the kernel, converted before returning to user space:
      ERESTARTSYS   → SA_RESTART → Retry / Otherwise → EINTR
      ERESTARTNOHAND → Blocked but no handler → Retry
      ERESTART_RESTARTBLOCK → Requires restart_block mechanism (e.g., nanosleep)

Unoverridability of SIGKILL and SIGSTOP

// kernel/signal.c
// SIGKILL (9) and SIGSTOP (19) cannot be:
//   - blocked (sigprocmask ignores them)
//   - ignored (SIG_IGN is ignored)
//   - caught by handler (sa_handler is ignored)
//
// This is a design principle: The system always has a last resort to regain control
// Even if a process blocks all signals, SIGKILL still takes effect

// Exception: Processes in TASK_UNINTERRUPTIBLE (D state)
//   If a process is stuck in D state (e.g., waiting for disk IO), SIGKILL cannot terminate it immediately
//   Must wait for IO to complete → Process returns to runnable state → Signal delivery takes effect
//   TASK_KILLABLE (5.14+) is an improvement: Allows waking up via SIGKILL

ptrace

ptrace allows a tracer process to control the execution of a tracee process. gdb (breakpoints/single-stepping), strace (syscall tracing), and seccomp user notification are all built on ptrace.

Core Operations

// kernel/ptrace.c
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

// Key requests:
PTRACE_ATTACH / PTRACE_SEIZE  // Attach to target process
PTRACE_DETACH                 // Detach
PTRACE_SYSCALL                // Stop at every syscall entry/exit
PTRACE_SINGLESTEP             // Single-step execution of one instruction
PTRACE_CONT                   // Continue execution
PTRACE_PEEKDATA / PTRACE_POKEDATA  // Read/write tracee memory
PTRACE_GETREGS / PTRACE_SETREGS    // Read/write registers
PTRACE_O_TRACESYSGOOD         // Mark syscall-stop for distinction (bit 7 of sig)
PTRACE_O_TRACEFORK / PTRACE_O_TRACEVFORK / PTRACE_O_TRACECLONE
PTRACE_O_TRACEEXIT
PTRACE_GET_SYSCALL_INFO       // Get detailed syscall info (5.3+)

Internal Mechanism

// Attachment flow:
ptrace(PTRACE_ATTACH, pid, ...)ptrace_attach()
    → Set task->ptrace = PT_PTRACED
    → Set task->parent = tracer (real parent is saved in real_parent)
    → Send SIGSTOP to tracee
    → Tracee enters TASK_TRACED state
    → Tracer can waitpid() to receive stop notification

// Signal interception:
// When tracee receives a signal, kernel checks task->ptrace:
//   → PT_PTRACED: Signal is first sent to tracer (tracer can modify or consume it)
//   → Tracer obtains signal info via waitpid()
//   → Tracer decides to let signal pass (PTRACE_CONT) or ignore

// Syscall tracing:
// PTRACE_SYSCALL → Set TIF_SYSCALL_TRACE
// → syscall entry: syscall_trace_enter() → ptrace_report_syscall()
//   → Tracee enters TASK_TRACED, tracer receives notification via waitpid
//   → Tracer returns → Tracee executes the syscall body
// → syscall exit: syscall_trace_exit() → Stop again
//   → Tracer can see the syscall return value

ptrace Access Modes

// PTRACE_PEEKDATA: Underlying interface via /proc/<pid>/mem
// Read one word at a time (traditional ptrace API, low efficiency)
// 
// More efficient method: Direct read/write via /proc/<pid>/mem (no ptrace attach needed!)
//   → process_vm_readv() / process_vm_writev()
//   → But can only read/write, cannot control execution

// Kernel implementation: ptrace_access_vm() → get_user_pages_remote()
//   → Map tracee's virtual address to its own address space via page tables
//   → Direct memory access (no copy_from_user needed)

Mutual Exclusion between ptrace and seccomp

seccomp (SECCOMP_SET_MODE_FILTER):
  A process can either be ptraced OR have a seccomp filter, but not both.
  Reason: Security — seccomp assumes "only you control your own syscall policy".
          If ptraced, the tracer can bypass seccomp via POKEDATA.

  SECCOMP_RET_USER_NOTIF (seccomp user notification):
    This is a "ptrace alternative under the seccomp framework".
    → Allows a supervisor process to check/approve syscalls from a sandboxed process.
    → Does not set PT_PTRACED, can coexist with seccomp filters.
    → Lighter than ptrace: Only intercepts syscalls, not signals.

Debugging and Observation

# View currently blocked / pending / ignored signals for a process
cat /proc/<pid>/status | grep -i sig
# SigBlk: Blocked signals
# SigIgn: Ignored signals
# SigCgt: Caught signals (have handler)
# SigPnd: Per-thread pending
# ShdPnd: Thread group shared pending

# View which process has the most pending signals (might be signal-bombarded)
for pid in /proc/[0-9]*; do
    pend=$(awk '/SigPnd:/{print $2}' $pid/status 2>/dev/null)
    [ "$pend" != "0000000000000000" ] && echo "$pid: $pend"
done

# Use strace to view signal delivery
strace -e trace=signal -p <pid>

# Send signals and view kernel trace
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_deliver/enable
cat /sys/kernel/debug/tracing/trace_pipe

References and Further Reading

  • Kernel Documentation: Documentation/process/adding-syscalls.rst (Interaction between signals and syscalls), man 7 signal, man 2 ptrace
  • LWN:
    • "A new API for signal handling" (lwn.net/Articles/414618/)
    • "Seccomp user notification" (lwn.net/Articles/785746/)
  • Source Files:
    • kernel/signal.c — send_signal(), get_signal(), do_signal()
    • kernel/ptrace.c — ptrace_attach(), ptrace_resume()
    • arch/x86/kernel/signal.c — handle_signal(), setup_rt_frame()
    • arch/x86/kernel/ptrace.c — Register read/write

Keywords: sigaction, sigpending, SIGKILL, SA_RESTART, TIF_SIGPENDING, sigframe, rt_sigreturn, PTRACE_SYSCALL, TASK_TRACED, seccomp