SA_RESTART and Signal-Interrupted Syscalls

When a thread is blocked inside a slow system call — say, parked in read() waiting on an idle TCP socket — and a signal arrives whose handler the thread has installed, the kernel must do something before it can run that handler: it has to unwind the thread out of the blocking call. What happens to the call afterward is governed almost entirely by a single bit, SA_RESTART, passed to sigaction(2) when the handler is established. With the bit set, the kernel transparently restarts the interrupted call after the handler returns, so the program never sees the interruption; without it, the call aborts with the error EINTR and the program must cope. The catch — and the reason this note exists — is that SA_RESTART does not restart everything. A large, surprising, and exactly-enumerated set of calls (anything with a timeout, all of poll/select/epoll_wait, every System V IPC and sleep interface) always fails with EINTR regardless of the flag, because restarting them cleanly is impossible. This note reproduces the signal(7) tables faithfully and explains why the line falls where it does.

This note owns the userspace surface of SA_RESTART: what the flag means to sigaction, which calls it does and does not save you from, and how to write code that survives interruption. The kernel-internal mechanism — the ERESTARTSYS, ERESTARTNOHAND, ERESTARTNOINTR, and ERESTART_RESTARTBLOCK magic return codes the handler path inspects, and how the entry code rewinds the instruction pointer — lives in its sibling Restartable System Calls and ERESTARTSYS. The plain “what is EINTR and why do I keep getting it” framing lives in EINTR and Interrupted System Calls. Read those for the layers below and around this one.

Mental Model

The key idea is that interruption is a fork in the road with three outcomes, and which branch is taken depends on (a) which syscall is blocked, (b) whether a user handler is even involved, and (c) whether SA_RESTART was set. SA_RESTART is only one of three inputs, and it only matters on one of the branches.

flowchart TB
  BLOCK["Thread blocked in a syscall<br/>(read on pipe, accept, futex,<br/>poll, nanosleep, ...)"]
  SIG["Signal becomes deliverable<br/>to this thread"]
  BLOCK --> SIG
  SIG --> KIND{"Is there a user-installed<br/>handler for this signal?"}
  KIND -->|"No handler<br/>(default action, or via<br/>restart-block path)"| RB["Kernel may auto-restart<br/>via restart_syscall<br/>(nr 219) — never seen<br/>by userspace"]
  KIND -->|"Handler runs"| CLASS{"Which class is<br/>the blocked call?"}
  CLASS -->|"Restartable class<br/>(read/write/wait/accept/<br/>recv with NO timeout, ...)"| FLAG{"SA_RESTART set?"}
  CLASS -->|"Never-restart class<br/>(poll/select/epoll, SysV IPC,<br/>nanosleep, anything with<br/>a timeout, ...)"| EINTR["Call fails with EINTR<br/>regardless of SA_RESTART"]
  FLAG -->|"Yes"| RESTART["Kernel restarts the call<br/>after handler returns —<br/>program never sees EINTR"]
  FLAG -->|"No"| EINTR

The three-way fork an interrupted syscall faces. What it shows: SA_RESTART is consulted only on the middle branch — a restartable call with a user handler. If there is no handler, the kernel can use its own restart-block machinery (the restart_syscall path, Restartable System Calls and ERESTARTSYS); if the call is in the never-restart class, EINTR is forced no matter what the flag says. The insight to take: setting SA_RESTART is necessary but never sufficient to make your program EINTR-proof — a correct program still needs an EINTR retry loop for the never-restart class, because no flag will ever silence those.

What SA_RESTART Means to sigaction

SA_RESTART is one of the sa_flags bits in the struct sigaction passed to sigaction(2). The man page’s own definition is deliberately terse (sigaction(2)):

Provide behavior compatible with BSD signal semantics by making certain system calls restartable across signals. This flag is meaningful only when establishing a signal handler. See signal(7) for a discussion of system call restarting.

Three phrases in that sentence carry the entire weight of the feature. “BSD signal semantics” is the historical anchor: classic System V signal() left interrupted calls failing with EINTR, while 4.2BSD made them restart automatically. Linux’s sigaction() lets you choose per-handler. “certain system calls” — not all — is the qualifier this whole note unpacks. And “meaningful only when establishing a signal handler” means the flag is a property of the handler registration, not of the syscall and not of the process: two handlers for two different signals can disagree on SA_RESTART, and whether a blocked call restarts depends on the disposition of the specific signal that interrupted it.

A subtle and frequently-tripped-over consequence: the obsolete signal(2) wrapper’s restart behavior is not portable. Per signal(2): “By default, in glibc 2 and later, the signal() wrapper function does not invoke the kernel system call. Instead, it calls sigaction(2) using flags that supply BSD semantics” — i.e. glibc’s signal() does set SA_RESTART, but the original System V signal() does not. The man page’s own remedy: “POSIX.1 solved the portability mess by specifying sigaction(2) … use that interface instead of signal().” If you want deterministic restart behavior, never use signal(); always use sigaction() and set the flag yourself.

The full set of sa_flags bits, for context (per sigaction(2)):

FlagMeaning
SA_RESTARTRestart certain interrupted syscalls instead of failing with EINTR (this note).
SA_NOCLDSTOPFor SIGCHLD: do not receive notification when children stop or resume.
SA_NOCLDWAITFor SIGCHLD: do not turn terminated children into zombies.
SA_NODEFERDo not automatically block the signal while its own handler runs.
SA_ONSTACKRun the handler on the alternate stack set by sigaltstack(2).
SA_RESETHANDReset disposition to SIG_DFL on handler entry (one-shot, BSD SA_ONESHOT).
SA_SIGINFOHandler takes three args (siginfo_t *, ucontext_t *) instead of one.
SA_RESTORERC-library-internal: address of the signal trampoline. Not for application use.
SA_UNSUPPORTED(Linux 5.11+) Probe whether the kernel honors flag bits.
SA_EXPOSE_TAGBITS(Linux 5.11+) Preserve arch tag bits in si_addr.

Uncertain

Verify: the precise kernel version that introduced SA_UNSUPPORTED and SA_EXPOSE_TAGBITS (“Linux 5.11”). Reason: taken from the man7.org sigaction(2) rendering (man-pages 6.18, dated 2026-02), not cross-checked against the kernel commit. To resolve: confirm against the kernel git history for SA_EXPOSE_TAGBITS / SA_UNSUPPORTED. These two are peripheral to this note’s topic. uncertain

The Calls That ARE Restarted by SA_RESTART

The following list is reproduced faithfully from the signal(7) man page, “Interruption of system calls and library functions by signal handlers” section, groff source fetched from the kernel man-pages git tree (man-pages 6.18, dated 2026-02-08). The page’s own framing: “If a blocked call to one of the following interfaces is interrupted by a signal handler, then the call is automatically restarted after the signal handler returns if the SA_RESTART flag was used; otherwise the call fails with the error EINTR.” Internally these all use the ERESTARTSYS return code (see Restartable System Calls and ERESTARTSYS).

  • read(2), readv(2), write(2), writev(2), and ioctl(2) calls on “slow” devices. A “slow” device is one where the I/O call may block for an indefinite time — for example, a terminal, pipe, or socket. If an I/O call on a slow device has already transferred some data by the time it is interrupted, the call returns a success status (normally the number of bytes transferred), not a restart and not EINTR. Crucially: a local disk is not a slow device by this definition — disk I/O is not interrupted by signals at all.
  • open(2), if it can block (e.g. opening a FIFO; see fifo(7)).
  • wait(2), wait3(2), wait4(2), waitid(2), and waitpid(2).
  • Socket interfaces: accept(2), connect(2), recv(2), recvfrom(2), recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2)unless a timeout has been set on the socket (see the never-restart list below).
  • File locking interfaces: flock(2) and the F_SETLKW and F_OFD_SETLKW operations of fcntl(2).
  • POSIX message queue interfaces: mq_receive(3), mq_timedreceive(3), mq_send(3), and mq_timedsend(3).
  • futex(2) FUTEX_WAIT (since Linux 2.6.22; beforehand it always failed with EINTR).
  • getrandom(2).
  • futex(2) FUTEX_WAIT_BITSET.
  • POSIX semaphore interfaces: sem_wait(3) and sem_timedwait(3) (since Linux 2.6.22; beforehand they always failed with EINTR).
  • read(2) from an inotify(7) file descriptor (since Linux 3.8; beforehand it always failed with EINTR).

Two of these entries are genuinely surprising and worth pausing on. mq_timedreceive/mq_timedsend and sem_timedwait are timed interfaces that nevertheless restart — yet the socket calls with SO_RCVTIMEO/SO_SNDTIMEO do not. The difference is the kind of timeout: the POSIX-mq and POSIX-semaphore timed variants take an absolute deadline (a struct timespec against CLOCK_REALTIME), so restarting the call with its original arguments lands on the same deadline — the call does the right thing automatically. A socket SO_RCVTIMEO is a relative duration stored on the socket, with no per-call deadline the kernel can preserve across a restart; restarting would reset the clock. That distinction — absolute deadline restartable, relative timeout not — is the single most useful rule of thumb for predicting which side of the line a timed call lands on.

The Calls That Are NEVER Restarted (always EINTR)

Reproduced faithfully from the same signal(7) section. The page’s framing: “The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR when interrupted by a signal handler.” Internally these use ERESTARTNOHAND or EINTR directly (see Restartable System Calls and ERESTARTSYS).

  • “Input” socket interfaces, when a receive timeout (SO_RCVTIMEO) has been set via setsockopt(2): accept(2), recv(2), recvfrom(2), recvmmsg(2) (also with a non-NULL timeout argument), and recvmsg(2).
  • “Output” socket interfaces, when a timeout (SO_RCVTIMEO) has been set via setsockopt(2): connect(2), send(2), sendto(2), and sendmsg(2).
  • Interfaces used to wait for signals: pause(2), sigsuspend(2), sigtimedwait(2), and sigwaitinfo(2).
  • File-descriptor multiplexing interfaces: epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), and pselect(2).
  • System V IPC interfaces: msgrcv(2), msgsnd(2), semop(2), and semtimedop(2).
  • Sleep interfaces: clock_nanosleep(2), nanosleep(2), and usleep(3).
  • io_getevents(2).

And one special case stated separately: sleep(3) is also never restarted if interrupted by a handler, but it gives a success return — the number of seconds remaining to sleep — rather than failing with EINTR.

Uncertain

Verify: the “Output socket interfaces” bullet in the never-restart list cites the timeout as SO_RCVTIMEO, but the send-side timeout option is SO_SNDTIMEO (and the parallel “stop signals” section of the same page does say SO_SNDTIMEO for the output case). This looks like a long-standing copy-paste typo in signal(7) itself, not a real semantic claim. Reason: the man page text says SO_RCVTIMEO for output sockets, which is almost certainly wrong; the send timeout is SO_SNDTIMEO. To resolve: read net/socket.c / net/ipv4/tcp.c timeout handling, or file/track a man-pages report. Treat “output sockets with a send timeout never restart” as the intended meaning. uncertain

The crucial takeaway for application code: poll, select, and epoll_wait always return EINTR on signal interruption. There is no flag that changes this. Event loops built on these must wrap them in a retry loop, or convert signals into file-descriptor events (signalfd(2)) / drain them via the loop’s own self-pipe so the signal never interrupts the wait in the first place. This is one of the most common real bugs in hand-rolled servers. A Linux-specific gotcha makes the naive retry loop subtly wrong for select(2): per select(2), “On Linux, select() modifies timeout to reflect the amount of time not slept,” and “also modifies timeout if the call is interrupted by a signal handler (i.e., the EINTR error return)” — behavior the page notes is “not permitted by POSIX.1.” So a do { } while (errno == EINTR) loop around select() that re-passes the same timeout struct will, on Linux, automatically resume against the shrunken remaining timeout (often what you want) — but the same loop is non-portable, and code that reuses the struct across unrelated select() calls will silently use a near-zero timeout. Use pselect/ppoll or recompute the deadline explicitly.

Why Some Calls Cannot Restart Cleanly

The deep reason behind the never-restart list is that a restarted syscall re-enters the kernel with its original argument registers — the kernel rewinds the user instruction pointer back to the syscall instruction and the same arguments are loaded again (the mechanism is detailed in Restartable System Calls and ERESTARTSYS and The x86-64 Syscall Calling Convention). For a call whose semantics are “block until something happens,” replaying the original arguments is harmless and correct: read(fd, buf, 4096) restarted is still a perfectly good read(fd, buf, 4096).

But for a call whose argument is a relative duration, replaying the original argument is wrong. Consider nanosleep(2) asked to sleep 10 seconds. A signal arrives after 7 seconds. If the kernel naively restarted the call with its original arguments, the thread would sleep another full 10 seconds — 17 in total — silently violating the contract. There is no way to fix this by just retrying the original arguments, so nanosleep cannot use the simple ERESTARTSYS restart path. Linux handles this with a separate restart-block mechanism, surfaced as the internal-only restart_syscall(2) (syscall number 219 on x86-64 — there is no glibc wrapper and userspace must never call it). Per its man page, restart_syscall “is used to restart certain system calls after a process that was stopped by a signal … is later resumed after receiving a SIGCONT,” and when it restarts the call it does so “with a time argument that is suitably adjusted to account for the time that has already elapsed (including the time where the process was stopped).” It applies to the relative-timeout calls — poll(2) (since Linux 2.6.24), nanosleep(2), clock_nanosleep(2), and futex(2) FUTEX_WAIT/FUTEX_WAIT_BITSET — precisely because those are the calls whose timeout must be decremented across the interruption rather than replayed whole. This is why restart_syscall exists at all. See Restartable System Calls and ERESTARTSYS for the ERESTART_RESTARTBLOCK / restart_block internals.

However — and this is the part that surprises people — the restart-block path does not silence interruption when a user handler runs. If you installed a handler for the interrupting signal (even with SA_RESTART), nanosleep does not transparently resume; it returns EINTR to userspace and, if you passed a non-NULL rem argument, writes the remaining time there so you can decide whether to re-sleep. From nanosleep(2): “If the call is interrupted by a signal handler, nanosleep() returns -1, sets errno to EINTR, and writes the remaining time into the structure pointed to by rem unless rem is NULL.” The same holds for clock_nanosleep(2) with a relative request (TIMER_ABSTIME not set), which returns the positive error number EINTR and the unslept remainder in remain. For an absolute clock_nanosleep request (TIMER_ABSTIME set), “the remain argument is unused, and unnecessary,” so there is no remainder to report, and the right retry is simply to call it again with the same absolute deadline — which is exactly why absolute timed calls are easier to make robust. (The restart_syscall deadline-adjustment path described above is what handles the stop/SIGCONT case, where the call is interrupted not by a handler but by the process being stopped and resumed.)

The socket-timeout calls are the same problem in different clothing: the SO_RCVTIMEO duration is relative and lives on the socket, with no per-call deadline the kernel can carry across a restart, so the kernel switches them to forced EINTR rather than risk restarting against a clock that has already partly elapsed.

There is one documented escape hatch in the other direction: the seccomp user-space notification feature can, in certain circumstances, cause restarting of system calls that SA_RESTART would otherwise never restart. Per seccomp_unotify(2): “system call restarting … will occur even for the blocking system calls listed in signal(7) that would never normally be restarted by the SA_RESTART flag.” This happens when a target makes a syscall that triggers a supervisor notification, then is hit by a signal while blocked waiting for the supervisor — the kernel restarts the intercepted call, so the supervisor may see multiple notifications for one logical call. This is a narrow, advanced case; see seccomp and Syscall Filtering.

Code: Establishing SA_RESTART and the Retry Loop You Still Need

#include <signal.h>
#include <errno.h>
#include <unistd.h>
 
static volatile sig_atomic_t got_signal = 0;
static void on_sig(int signo) { got_signal = 1; }
 
int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = on_sig;          /* simple handler                       */
    sigemptyset(&sa.sa_mask);        /* don't block extra signals in handler */
    sa.sa_flags = SA_RESTART;        /* ask the kernel to restart the        */
                                     /* restartable class of calls           */
    sigaction(SIGUSR1, &sa, NULL);   /* USE sigaction, never signal()        */
 
    char buf[4096];
    /* read() on a pipe/socket is in the RESTARTABLE class, so with
       SA_RESTART set this loop will essentially never observe EINTR —
       the kernel restarts it transparently.                              */
    ssize_t n = read(STDIN_FILENO, buf, sizeof buf);
    /* ... but for a NEVER-restart call, the flag does nothing:           */
    return (n < 0 && errno == EINTR) ? 1 : 0;
}

Line-by-line: sa_handler points at the handler; sigemptyset(&sa.sa_mask) declares no additional signals are blocked during the handler (the interrupting signal itself is auto-blocked unless SA_NODEFER); sa.sa_flags = SA_RESTART is the one bit this note is about; and sigaction() installs it. Because read() on a pipe is in the restartable class, that single read() will be auto-restarted and rarely if ever return EINTR.

For the never-restart class — the one SA_RESTART cannot help — the canonical idiom is the EINTR retry loop, often hidden behind a macro (glibc even provides TEMP_FAILURE_RETRY):

int ret;
do {
    ret = poll(fds, nfds, timeout_ms);   /* poll() NEVER restarts —        */
} while (ret == -1 && errno == EINTR);    /* SA_RESTART is irrelevant here  */

This loop is mandatory for poll/select/epoll_wait, the SysV IPC calls, the sleep calls (where you should re-sleep the remainder, not the original duration), and any socket call with a timeout set. Setting SA_RESTART does not let you delete it.

Failure Modes and Common Misunderstandings

  • “I set SA_RESTART, so I don’t need to handle EINTR.” Wrong for the entire never-restart list. The most common production form: an epoll/poll event loop that spuriously wakes and mis-handles the -1/EINTR return when any signal fires (e.g. a SIGCHLD from a reaped child, or SIGWINCH).
  • signal() vs sigaction() restart drift. Code using the old signal() wrapper inherits glibc’s BSD default (SA_RESTART on), but the same code linked against a different libc, or ported to System V semantics, gets the opposite. Symptom: a read() that “never gets EINTR” on Linux/glibc suddenly does elsewhere. Fix: use sigaction() explicitly.
  • Partial transfers on slow devices. A read()/write() on a pipe or socket interrupted after transferring some bytes does not restart and does not return EINTR — it returns the short count. Code that assumes a read() either fully completes or returns EINTR mishandles the short-read case.
  • Sleeping the wrong amount after interruption. Re-calling nanosleep() with the original duration instead of *rem over-sleeps. Always re-sleep the remainder (relative) or call against the same absolute deadline.
  • SO_RCVTIMEO/SO_SNDTIMEO silently changes restart behavior. Adding a socket timeout moves recv/send/accept/connect from the restartable class into the never-restart class. Code that relied on SA_RESTART to paper over interruption breaks the moment someone sets a socket timeout.

Alternatives and When to Choose Them

  • signalfd(2) — turn signals into readable file-descriptor events and add the fd to your poll/epoll set. The wait call is then never interrupted by these signals because they are blocked and delivered synchronously as fd reads. Preferred for event loops; it sidesteps the whole SA_RESTART/EINTR question for the loop’s wait call. See seccomp and Syscall Filtering’s neighbor topics in Linux IPC MOC.
  • The self-pipe trick — the pre-signalfd portable idiom: the handler writes one byte to a pipe whose read end is in the select/poll set, converting “signal interrupted my wait” into “there is data on the self-pipe.”
  • ppoll(2)/pselect(2) with a signal mask — atomically unblock signals only for the duration of the wait, closing the race between checking a flag and blocking. Still returns EINTR (they are in the never-restart list), but the atomic mask swap is what you actually want when mixing signals and multiplexing.
  • Just set SA_RESTART and write the loop anyway — for simple programs doing blocking read/write, SA_RESTART plus a defensive EINTR loop is the lowest-effort correct approach.

Production Notes

The EINTR-after-SIGCONT wrinkle bites real software: even with no signal handler installed, on Linux certain blocking calls fail with EINTR after the process is stopped (SIGSTOP/SIGTSTP) and resumed via SIGCONT — a behavior signal(7) explicitly notes is not sanctioned by POSIX.1 and does not occur on other systems. The affected set per the man page includes the timeout-bearing socket calls, epoll_wait/epoll_pwait, semop/semtimedop, and sigtimedwait/sigwaitinfo. This is why even a daemon that installs zero handlers must still tolerate EINTR on these calls: a developer suspending it under a debugger or shell job control can inject EINTR out of nowhere. The lesson, repeated across decades of systems code, is that the EINTR retry loop is not optional defensive paranoia — it is part of the contract.

See Also