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 sigset_t;
// Common operations
; // Clear
; // Add
; // Remove
; // Test
Per-process Signal Structures
// include/linux/sched.h
;
// include/linux/sched/signal.h
;
;
// include/linux/sched/signal.h
;
sigaction — Signal Handling Action
// include/linux/signal_types.h
;
// 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
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
// If handler != SIG_DFL/SIG_IGN:
static void
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 ;
// 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_GET_SYSCALL_INFO // Get detailed syscall info (5.3+)
Internal Mechanism
// Attachment flow:
→
→ Set task->ptrace = PT_PTRACED
→ Set task->parent =
→ Send SIGSTOP to tracee
→ Tracee enters TASK_TRACED state
→ Tracer can 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
|
# 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; do
pend=
[ &&
done
# Use strace to view signal delivery
# Send signals and view kernel trace
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