Restartable System Calls and ERESTARTSYS
When a signal arrives while a process is blocked deep inside a system call, the kernel faces a choice: abort the call and report
-EINTRto userspace, or transparently re-run the call so the program never notices the interruption. Linux implements both behaviors with a small family of kernel-internal error codes —ERESTARTSYS(512),ERESTARTNOINTR(513),ERESTARTNOHAND(514), andERESTART_RESTARTBLOCK(516) — that a blocked syscall handler returns instead of-EINTR. These codes never reach userspace; they are instructions to the signal-delivery machinery. On the way back out to user mode, the architecture’s signal code inspects the code, the pending signal’sSA_RESTARTflag, and whether a handler will run, and then either rewinds the instruction pointer to re-issue thesyscallinstruction or converts the code to-EINTR. The kernel header that defines them states the contract bluntly: “These should never be seen by user programs” (include/linux/errno.h, v6.12).
This note is the kernel-side mechanism. Its sibling EINTR and Interrupted System Calls covers what userspace sees and must do; SA_RESTART and Signal-Interrupted Syscalls covers the per-handler flag whose value this machinery reads. What this note owns that its large neighbours do not: The Generic Syscall Entry and Exit Layer owns the entry/exit work loop and the SYSCALL_WORK_*/TIF_* gating that reaches the restart decision, but stops at the call to arch_do_signal_or_restart(); this note owns everything inside that call — the four restart verbs, the two resolution paths, and the restart_block continuation machinery. libc Syscall Wrappers and errno Translation owns the userspace wrapper that turns the resulting -EINTR into errno; it never sees an ERESTART* code at all. The errno Convention and Negative Return Values owns the numeric convention these codes live inside, including why 512 is legal as an internal code but illegal as a userspace-visible one.
Version pin. Every code claim below is read from the Linux 6.12 LTS tree, with the 6.18 LTS tree fetched at the same paths for comparison (kernel.org/releases.json lists both 6.18 and 6.12 as longterm as of 2026-09-12; 6.12 is this vault’s pin). The internal-code list in include/linux/errno.h is byte-for-byte identical between the two tags (verified by diff). arch/x86/kernel/signal.c is not byte-for-byte identical — v6.18 differs in three lines, all of them the FPU accessor change from current->thread.fpu to x86_task_fpu(current) — but the restart switch statements in handle_signal() and arch_do_signal_or_restart() are unchanged. include/linux/restart_block.h differs only in a comment, and include/linux/err.h gains one macro (IOMEM_ERR_PTR). So the mechanism described here is current for both LTS lines.
Everything in this note that can be measured, was measured. The per-syscall restart-code table and the SA_RESTART truth table below are not transcribed from documentation — they were produced by running programs on Fedora 44 (kernel 7.1.13-200.fc44.x86_64, glibc 2.43-8.fc44, GCC 16.2.1, GDB 17.2) and reading the raw internal code out of RAX through the ptrace syscall-exit trap. The source and output are in Demonstrating It.
Mental Model
Think of a blocking syscall as a function that may be suspended mid-flight. While it sleeps — waiting for bytes on a pipe, for a child to exit, for a futex to be woken — a signal can become pending on the task. The scheduler’s signal_pending() check inside the sleep wakes the syscall early. At that point the handler cannot return real data (it has none), so instead of -EINTR it returns one of the ERESTART* codes. That code is a verb, not an error:
ERESTARTNOINTR— “always restart me, no matter what.”ERESTARTSYS— “restart me ifSA_RESTARTis set on the signal’s handler; otherwise give userspaceEINTR.”ERESTARTNOHAND— “restart me if no handler runs for this signal; otherwiseEINTR.”ERESTART_RESTARTBLOCK— “restart me, but throughrestart_syscall()with time-adjusted arguments (I had an absolute or relative deadline).”
The decision is not made by the syscall. It is made later, once, on the return-to-userspace path, where the kernel knows the full picture: which signal is being delivered, with what sigaction flags, and whether a userspace handler is about to run.
flowchart TB BLK["Blocked syscall<br/>(read/wait/futex/nanosleep)"] SIG["Signal becomes pending<br/>(signal_pending() true)"] RET["Handler returns -ERESTART*<br/>(NOT -EINTR)"] EXIT["exit_to_user_mode_loop<br/>sees _TIF_SIGPENDING"] DISP["arch_do_signal_or_restart()"] GS{"get_signal():<br/>handler to run?"} HS["handle_signal()<br/>(a handler runs)"] NH["no-handler path"] DEC1{"which ERESTART<br/>code? SA_RESTART?"} DEC2{"which ERESTART<br/>code?"} EINTR["regs->ax = -EINTR<br/>(syscall fails)"] REISSUE["regs->ax = orig_ax<br/>regs->ip -= 2<br/>(re-run syscall insn)"] RBLK["regs->ax = __NR_restart_syscall<br/>regs->ip -= 2<br/>(re-run via restart_syscall)"] BLK --> SIG --> RET --> EXIT --> DISP --> GS GS -->|yes| HS --> DEC1 GS -->|no| NH --> DEC2 DEC1 -->|"ERESTART_RESTARTBLOCK<br/>or ERESTARTNOHAND;<br/>ERESTARTSYS w/o SA_RESTART"| EINTR DEC1 -->|"ERESTARTNOINTR;<br/>ERESTARTSYS w/ SA_RESTART"| REISSUE DEC2 -->|"ERESTARTNOINTR/SYS/NOHAND"| REISSUE DEC2 -->|"ERESTART_RESTARTBLOCK"| RBLK
The restart decision tree on x86-64, transcribed from handle_signal() and arch_do_signal_or_restart() in arch/x86/kernel/signal.c. What it shows: the same four ERESTART* codes are resolved by two different code paths — one when a handler will run, one when no handler runs — and the resolution differs between them. The insight: “restart” is decided at signal-delivery time, not at syscall time, and the most subtle case is ERESTART_RESTARTBLOCK, which the handler path converts to EINTR but the no-handler path restarts through a different syscall (restart_syscall). This single asymmetry explains why timeout-bearing calls like nanosleep are “never restarted with SA_RESTART.”
Where the ERESTART Code Comes From
A blocking primitive deep in the kernel — wait_event_interruptible, schedule_timeout_interruptible, a futex wait, an hrtimer sleep — wakes whenever signal_pending(current) becomes true. By convention it then returns -ERESTARTSYS to its caller, which propagates up to the syscall wrapper. The errno header is explicit that this is only legal when a signal is actually pending: “To return one of ERESTART* codes, signal_pending() MUST be set” (errno.h, v6.12). The same header confirms the numeric values, which sit just above the userspace-visible errno range (the errno(3) values stop in the low hundreds; ERESTARTSYS is 512):
#define ERESTARTSYS 512
#define ERESTARTNOINTR 513
#define ERESTARTNOHAND 514 /* restart if no handler.. */
#define ERESTART_RESTARTBLOCK 516 /* restart by calling sys_restart_syscall */These four codes are byte-for-byte identical between the 6.12 and 6.18 trees (errno.h, v6.18). Because they are numerically above 511, the libc convention that treats a return in [-4095, -1] as a negated errno would not obviously distinguish them — which is exactly why the kernel guarantees they are intercepted before the syscall ever returns to userspace.
The Four Restart Verbs, Side by Side
The single most important fact about these codes is that the same code resolves differently depending on whether a userspace handler is about to run, and only one of the four even looks at SA_RESTART. This table is the whole note in miniature; the sections that follow derive each column from source.
| Code | Value | Literal meaning | Handler runs (handle_signal) | No handler runs (arch_do_signal_or_restart tail) | Consults SA_RESTART? | Canonical kernel users (verified v6.12) |
|---|---|---|---|---|---|---|
ERESTARTSYS | 512 | “restart me if the program said it wants restarts” | SA_RESTART set → re-issue; clear → -EINTR | re-issue (ax = orig_ax, ip -= 2) | yes — the only one | wait_event_interruptible and everything built on it; wait_for_completion_interruptible; futex_wait with no timeout; read/write on a pipe/tty/socket; wait4; sock_intr_errno() when no socket timeout is set |
ERESTARTNOINTR | 513 | “restart me unconditionally” | re-issue, ignoring SA_RESTART | re-issue | no | copy_process() — fork/clone/vfork bail out with this if a signal is already pending, so the fork is always retried |
ERESTARTNOHAND | 514 | “restart me only if no handler runs” | -EINTR, always | re-issue | no | pause, sigsuspend/rt_sigsuspend, core_sys_select (so select/pselect6), do_sys_poll (before sys_poll upgrades it), clock_nanosleep(TIMER_ABSTIME) |
ERESTART_RESTARTBLOCK | 516 | “restart me through restart_syscall(), which knows my adjusted deadline” | -EINTR, always | re-issue as a different syscall: ax = __NR_restart_syscall | no | nanosleep/clock_nanosleep (relative), poll/ppoll via do_restart_poll, futex_wait with a timeout, alarmtimer, posix-cpu-timers |
The four restart verbs and their two resolutions. What it shows: three of the four codes have an unconditional answer on the handler path — two of them -EINTR, one of them “always restart” — and only ERESTARTSYS is a question whose answer SA_RESTART supplies. The insight to take: SA_RESTART is not a global “make my syscalls restartable” switch; it is a single bit consulted in exactly one case label of one switch statement. A call that returns any of the other three codes is completely indifferent to it. The whole “which syscalls does SA_RESTART cover?” question reduces to “which syscalls return ERESTARTSYS?”
515, 517, 518, 519 — the codes that are not about restarting
include/linux/errno.h holds more than the restart family. The full v6.12 list runs 512–519 and then 521–531, and the header’s “should never be seen by user programs” contract covers all of them, not just the ERESTART* four. The non-restart members are worth naming because they are the most common source of confusion about what the 512+ range is for:
| Code | Value | Purpose | Who consumes it |
|---|---|---|---|
ENOIOCTLCMD | 515 | “this ioctl command is not mine” — a driver’s way of saying “fall through to generic handling” | the VFS/driver ioctl dispatch, which translates it to -ENOTTY for userspace |
EPROBE_DEFER | 517 | “a resource this driver needs does not exist yet; re-probe me later” | the driver core’s deferred-probe list |
EOPENSTALE | 518 | “open() raced with a rename/unlink and hit a stale dentry; retry the lookup” | path_openat()’s retry loop |
ENOPARAM | 519 | “this mount parameter is not one of mine” | the fs_context parameter parser |
EBADHANDLE … ENOGRACE | 521–531 | NFS/NFSv3-protocol-internal conditions (note 520 is unallocated) | the NFS client/server, which map them to on-the-wire NFS status codes |
The rest of the kernel-internal errno space at v6.12. What it shows: the 512+ range is a general-purpose “private error channel” between kernel subsystems, of which syscall restarting is only the first and most famous user. The insight to take: none of these values can ever reach userspace, and the reason is structural rather than a matter of discipline — see the header-split argument in The errno Convention and Negative Return Values. They are also not all “retry” codes: ENOIOCTLCMD and ENOPARAM are “not mine, ask someone else,” which is a different idea wearing the same numeric clothing.
Where in the kernel the code is actually produced
The ERESTART* code is almost never written by hand in a syscall handler. It comes out of a sleeping primitive that noticed signal_pending(current), and the primitive’s documented return value is the convention:
-
wait_event_interruptible()— the workhorse. Its kerneldoc is explicit: “The function will return-ERESTARTSYSif it was interrupted by a signal and 0 if @condition evaluated to true” (include/linux/wait.h, v6.12). Everything layered on it —wait_event_interruptible_timeout, waitqueue sleeps in drivers, pipe and socket reads — inherits that. -
wait_for_completion_interruptible()— “Return:-ERESTARTSYSif interrupted, 0 if completed” (kernel/sched/completion.c, v6.12). -
schedule_timeout_interruptible()and the hrtimer sleepers — these return whether the timer expired, and the caller turns “woke early with a signal pending” into-ERESTART_RESTARTBLOCK(seedo_nanosleep()below). -
sock_intr_errno()— a one-line decision function that is the single cleanest statement of the whole timeout rule in the tree (include/net/sock.h, v6.12):/* Alas, with timeout socket operations are not restartable. * Compare this to poll(). */ static inline int sock_intr_errno(long timeo) { return timeo == MAX_SCHEDULE_TIMEOUT ? -ERESTARTSYS : -EINTR; }Read it slowly, because it explains an entire bullet of signal(7).
timeois the socket’s effective timeout in jiffies;MAX_SCHEDULE_TIMEOUTis the sentinel for “no timeout at all.” If there is no timeout, the socket call returns-ERESTARTSYSandSA_RESTARTworks normally. IfSO_RCVTIMEOorSO_SNDTIMEOis set, the call returns plain-EINTR— it never enters the restart machinery in the first place. That is why signal(7) lists the socket calls in both of its lists, distinguished only by whether a timeout has been set.
flowchart LR subgraph P["1. the sleeping primitive notices signal_pending()"] W1["wait_event_interruptible()<br/>→ -ERESTARTSYS"] W2["wait_for_completion_interruptible()<br/>→ -ERESTARTSYS"] W3["do_nanosleep() / hrtimer sleepers<br/>→ -ERESTART_RESTARTBLOCK"] W4["sock_intr_errno(timeo)<br/>→ -ERESTARTSYS or plain -EINTR"] W5["mutex_lock_interruptible()<br/>→ -EINTR (different convention!)"] end subgraph S["2. the syscall wrapper may UPGRADE or DOWNGRADE it"] U1["sys_poll: 514 → set_restart_fn()<br/>→ 516"] U2["futex_wait: 512 → set_restart_fn()<br/>→ 516 (only if a timeout was given)"] U3["hrtimer_nanosleep: 516 → 514<br/>if HRTIMER_MODE_ABS"] U4["poll_select_finish: 514 → -EINTR<br/>only if the timeout writeback faulted"] end subgraph R["3. regs->ax carries the final code out"] OUT["-512 / -513 / -514 / -516<br/>or a plain -EINTR that<br/>neither switch will match"] end W1 --> S W2 --> S W3 --> S W4 --> OUT W5 --> OUT U1 --> OUT U2 --> OUT U3 --> OUT U4 --> OUT
How a restart code is produced, in two stages. What it shows: the sleeping primitive picks a code, and then the syscall’s own wrapper is free to change it — poll turns 514 into 516, futex turns 512 into 516 but only when a timeout is present, and absolute clock_nanosleep turns 516 back into 514. The insight to take: you cannot predict a syscall’s restart behaviour by looking only at what it sleeps on; the wrapper gets the last word, and that is exactly why poll and select — which sleep on the same do_select-family machinery — end up with different codes and different no-handler behaviour. It is also why a plain -EINTR from mutex_lock_interruptible() bypasses the whole apparatus: it never enters stage 2 and matches nothing in stage 3.
A trap for driver authors: the interruptible sleeping primitives do not all use the same convention. mutex_lock_interruptible() documents “Return: 0 if the lock was successfully acquired or -EINTR if a signal arrived” (kernel/locking/mutex.c, v6.12) — -EINTR, not -ERESTARTSYS. A driver that forwards a mutex_lock_interruptible() failure straight out of its ioctl has silently made that path non-restartable even under SA_RESTART. Converting it (if (mutex_lock_interruptible(&m)) return -ERESTARTSYS;) is a real and common idiom precisely because of this mismatch.
The Return-to-Userspace Hook
The ERESTART* code rides back through the generic syscall-exit machinery as the value stashed in the pt_regs accumulator register (regs->ax on x86-64). Returning to user mode is not a single ret; it runs exit_to_user_mode_loop() in kernel/entry/common.c, which drains pending thread-info work before crossing the privilege boundary (see Returning to Userspace and exit_to_user_mode and Thread Info Flags and Syscall Exit Work). The relevant line:
if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
arch_do_signal_or_restart(regs);_TIF_SIGPENDING is the thread-info flag that says “a signal is queued for this task”; _TIF_NOTIFY_SIGNAL is its kernel-internal cousin (used by, e.g., io_uring task work). Either one routes into arch_do_signal_or_restart(), the architecture entry point that both delivers the signal and applies the restart decision. That single call is where the ERESTART* code is resolved.
The Two Resolution Paths
arch_do_signal_or_restart() (in arch/x86/kernel/signal.c) calls get_signal(&ksig). get_signal() (in kernel/signal.c) dequeues the next deliverable signal and decides its disposition; it returns true if a userspace handler must run and fills in ksig (the struct ksignal, carrying the sigaction in ksig.ka.sa). The two outcomes — handler vs. no handler — are resolved by two different blocks of code, and the difference between them is the whole subject of this note.
Path A — a handler will run (handle_signal)
When get_signal() returns true, handle_signal() runs. Before it builds the signal frame on the user stack, it fixes up the syscall result:
/* Are we from a system call? */
if (syscall_get_nr(current, regs) != -1) {
/* If so, check system call restarting.. */
switch (syscall_get_error(current, regs)) {
case -ERESTART_RESTARTBLOCK:
case -ERESTARTNOHAND:
regs->ax = -EINTR;
break;
case -ERESTARTSYS:
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR;
break;
}
fallthrough;
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
}
}Walking it symbol by symbol:
syscall_get_nr(current, regs) != -1— we only fix up if we were in a syscall (the entry path stamped the syscall number intoregs->orig_ax;-1means “not from a syscall,” e.g. an interrupted page fault).syscall_get_error(...)reads the negative value the handler left inregs->ax.ERESTART_RESTARTBLOCKandERESTARTNOHAND→regs->ax = -EINTR. A handler is running, so these becomeEINTR. This is the key fact:ERESTART_RESTARTBLOCK(used by timeout calls) is unconditionally converted toEINTRwhenever a handler runs — theSA_RESTARTflag is never even consulted for it on this path.ERESTARTSYS→ restart only ifksig->ka.sa.sa_flags & SA_RESTART; otherwiseEINTR. This is the textbookSA_RESTARTbehavior — and the gate is read from this specific signal’ssigaction, so two signals delivered to the same blocked call can resolve differently.ERESTARTNOINTR(andERESTARTSYSthat fell through withSA_RESTART) → restart unconditionally:regs->ax = regs->orig_axputs the syscall number back intoax(it had been clobbered with the-ERESTARTcode), andregs->ip -= 2rewinds the instruction pointer.
The regs->ip -= 2 rewind
On x86-64 the syscall instruction is two bytes (0F 05). When the trap was taken, the saved RIP pointed at the instruction after syscall. Subtracting 2 moves it back onto the syscall instruction itself, so that when the signal handler returns (via sigreturn) and execution resumes, the CPU re-executes syscall — with the original number restored in ax and the original arguments still in their registers (the kernel preserved them in pt_regs). From the program’s point of view, the call simply took longer; it never sees the interruption. This is transparent restart. Note the dependency on the instruction being exactly two bytes — the same trick on other architectures rewinds by that arch’s syscall-instruction width.
Path B — no handler runs (arch_do_signal_or_restart tail)
If get_signal() returns false (the signal was ignored, was a stop/continue handled by the kernel, or the task is simply about to re-check), no frame is built, and the tail of arch_do_signal_or_restart() runs:
/* Did we come from a system call? */
if (syscall_get_nr(current, regs) != -1) {
/* Restart the system call - no handlers present */
switch (syscall_get_error(current, regs)) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
case -ERESTART_RESTARTBLOCK:
regs->ax = get_nr_restart_syscall(regs);
regs->ip -= 2;
break;
}
}Here the resolution differs in two ways. First, ERESTARTNOHAND now restarts (its whole meaning is “restart if no handler”), and ERESTARTSYS restarts regardless of SA_RESTART (there is no sigaction to consult — no handler is running). Second, and crucially, ERESTART_RESTARTBLOCK does not restore orig_ax. Instead it sets regs->ax = get_nr_restart_syscall(regs), which returns __NR_restart_syscall (or its ia32/x32 variant). After the ip -= 2 rewind, the re-executed syscall instruction therefore invokes a different syscall — restart_syscall() — not the original one.
The whole trip, as a sequence
sequenceDiagram autonumber participant U as User code participant E as entry_SYSCALL_64<br/>(entry_64.S) participant H as syscall handler<br/>(e.g. pipe_read) participant X as syscall_exit_to_user_mode<br/>(kernel/entry/common.c) participant S as arch_do_signal_or_restart<br/>(arch/x86/kernel/signal.c) participant G as get_signal<br/>(kernel/signal.c) U->>E: syscall instruction (2 bytes, 0F 05) Note over E: orig_ax = nr<br/>ax = -ENOSYS (pre-loaded) E->>H: dispatch through the syscall table Note over H: wait_event_interruptible():<br/>sleeps in TASK_INTERRUPTIBLE H-->>H: another task calls kill() →<br/>TIF_SIGPENDING set, sleeper woken H->>X: return -ERESTARTSYS (regs->ax = -512) Note over X: ptrace syscall-exit trap fires HERE —<br/>a tracer can read -512 out of RAX X->>S: ti_work & _TIF_SIGPENDING S->>G: get_signal(&ksig) alt a userspace handler is installed G-->>S: true, ksig.ka.sa.sa_flags carries SA_RESTART Note over S: handle_signal():<br/>SA_RESTART set → ax = orig_ax, ip -= 2<br/>SA_RESTART clear → ax = -EINTR S->>U: build signal frame, jump to handler U->>U: handler returns → rt_sigreturn Note over U: RIP now points AT the syscall insn U->>E: the same 2 bytes execute again else no handler (ignored / stop / continue) G-->>S: false Note over S: tail switch:<br/>NOHAND/SYS/NOINTR → ax = orig_ax, ip -= 2<br/>RESTARTBLOCK → ax = __NR_restart_syscall S->>U: return to user mode, no frame built U->>E: the same 2 bytes execute again end
One interrupted read() from the syscall instruction to the re-executed syscall instruction. What it shows: the restart decision is taken in step 9/12, long after the handler has given up, and the only state it acts on is three fields of pt_regs (ax, orig_ax, ip) plus one bit of the delivered signal’s sigaction. The insight to take: userspace never observes a restart happening; from the program’s point of view the syscall instruction at that address simply took longer, and (on the handler path) its signal handler ran somewhere in the middle. Notice also that the ptrace exit trap in step 8 fires before the resolution in step 9 — that ordering is what makes the internal codes observable to a debugger and to nothing else, and it is what the Demonstrating It measurements below exploit.
The SA_RESTART Truth Table
This is the table the documentation does not print, and the reason SA_RESTART confuses people. There are three independent inputs — which ERESTART* code the handler returned, whether a userspace handler is about to run, and whether that handler’s sigaction has SA_RESTART — and the outcome is fully determined by the two switch statements quoted above.
ERESTART* code | Handler will run? | SA_RESTART set? | Code path taken | Outcome userspace sees |
|---|---|---|---|---|
ERESTARTSYS (512) | yes | yes | handle_signal, case -ERESTARTSYS → fallthrough | transparent restart — the same syscall re-runs |
ERESTARTSYS (512) | yes | no | handle_signal, case -ERESTARTSYS → regs->ax = -EINTR | -1 / EINTR |
ERESTARTSYS (512) | no | (irrelevant) | tail switch, case -ERESTARTSYS | transparent restart |
ERESTARTNOINTR (513) | yes | yes | handle_signal, case -ERESTARTNOINTR | transparent restart |
ERESTARTNOINTR (513) | yes | no | same case — the flag is never tested | transparent restart (uninterruptible by design) |
ERESTARTNOINTR (513) | no | (irrelevant) | tail switch | transparent restart |
ERESTARTNOHAND (514) | yes | yes | handle_signal, case -ERESTARTNOHAND → regs->ax = -EINTR | -1 / EINTR — SA_RESTART does not help |
ERESTARTNOHAND (514) | yes | no | same case | -1 / EINTR |
ERESTARTNOHAND (514) | no | (irrelevant) | tail switch | transparent restart |
ERESTART_RESTARTBLOCK (516) | yes | yes | handle_signal, case -ERESTART_RESTARTBLOCK → regs->ax = -EINTR | -1 / EINTR — SA_RESTART does not help |
ERESTART_RESTARTBLOCK (516) | yes | no | same case | -1 / EINTR |
ERESTART_RESTARTBLOCK (516) | no | (irrelevant) | tail switch, ax = get_nr_restart_syscall(regs) | restart through restart_syscall(), with the saved deadline |
The complete SA_RESTART decision matrix for x86-64 at v6.12, derived from the two switch statements and confirmed row-by-row by the measurements in Demonstrating It. What it shows: SA_RESTART appears in exactly two of the twelve rows, and changes the outcome in exactly those two. Every other row is decided before the flag is consulted. The insight to take: the folk model “set SA_RESTART and my syscalls will restart” is right for one of four codes and wrong for the other three. Worse, the two codes that ignore it (NOHAND, RESTARTBLOCK) are precisely the ones used by the timeout-bearing calls people most want to protect — poll, select, nanosleep, clock_nanosleep. And note the counter-intuitive middle band: the three “no handler” rows all restart, which is why a SIGSTOP/SIGCONT pair usually does not break a blocked read(), while a caught SIGWINCH from a terminal resize does.
flowchart TD START["regs->ax holds an<br/>-ERESTART* code"] --> Q1{"get_signal():<br/>will a userspace<br/>handler run?"} Q1 -->|"no"| B{"which code?"} B -->|"512 / 513 / 514"| BRE["ax = orig_ax<br/>ip -= 2<br/>RE-ISSUE SAME SYSCALL"] B -->|"516"| BRB["ax = __NR_restart_syscall<br/>ip -= 2<br/>RE-ISSUE AS restart_syscall()"] Q1 -->|"yes"| A{"which code?"} A -->|"514 or 516"| AEI["ax = -EINTR<br/>SA_RESTART NEVER READ"] A -->|"513"| ARE["ax = orig_ax<br/>ip -= 2<br/>RE-ISSUE"] A -->|"512"| Q2{"ksig->ka.sa.sa_flags<br/>& SA_RESTART ?"} Q2 -->|"set"| ARE Q2 -->|"clear"| AEI AEI --> UEI["userspace: read() == -1,<br/>errno == EINTR"] ARE --> UOK["userspace: never notices"] BRE --> UOK BRB --> UOK2["userspace: never notices,<br/>and the deadline was honoured"]
The same matrix as a decision tree, reading top to bottom the way the kernel executes it. What it shows: there is exactly one diamond in the whole graph that mentions SA_RESTART, and reaching it requires both “a handler will run” and “the code was ERESTARTSYS”. The insight to take: this graph is the answer to “why did my SA_RESTART not work?” — trace your call’s code to one of the four leaves and you will almost always find you landed on 514/516, where the flag is not consulted, or that no handler was going to run at all and the call restarted for a completely different reason.
Which Code Does Each Syscall Return? Measured, Not Guessed
signal(7) publishes the behaviour (restartable vs. never restarted) but not the mechanism (which internal code produces it). The mapping can be read out of the source, and it can also be observed directly: a ptrace tracer stopped at the syscall-exit trap sees the raw internal code in RAX before arch_do_signal_or_restart() rewrites it. The table below gives both — the code path from the v6.12 source, and the raw RAX value actually measured with gdb on this machine (method and full transcript in Demonstrating It).
| Syscall (blocked, interrupted by a caught signal) | Raw RAX measured | Code | Source of the code | signal(7) classification |
|---|---|---|---|---|
read on a pipe | −512 | ERESTARTSYS | wait_event_interruptible in the pipe read path | restartable with SA_RESTART |
wait4 | −512 | ERESTARTSYS | do_wait()’s interruptible sleep | restartable with SA_RESTART |
futex(FUTEX_WAIT), timeout == NULL | −512 | ERESTARTSYS | __futex_wait() returns it directly | restartable with SA_RESTART |
futex(FUTEX_WAIT), timeout != NULL | −516 | ERESTART_RESTARTBLOCK | futex_wait() converts the 512 via set_restart_fn(restart, futex_wait_restart) | never restarted |
nanosleep (relative) | −516 | ERESTART_RESTARTBLOCK | do_nanosleep() → hrtimer_nanosleep() → set_restart_fn(..., hrtimer_nanosleep_restart) | never restarted |
poll | −516 | ERESTART_RESTARTBLOCK | do_sys_poll() returns 514; SYSCALL_DEFINE3(poll) upgrades it via set_restart_fn(..., do_restart_poll) | never restarted |
select | −514 | ERESTARTNOHAND | core_sys_select() returns it; poll_select_finish() lets it through | never restarted |
pause | −514 | ERESTARTNOHAND | SYSCALL_DEFINE0(pause) returns it literally | never restarted |
clock_nanosleep(TIMER_ABSTIME) | −514 | ERESTARTNOHAND | hrtimer_nanosleep()’s mode == HRTIMER_MODE_ABS branch downgrades 516 → 514 | never restarted |
ppoll | (not separately measured) | ERESTARTNOHAND | SYSCALL_DEFINE5(ppoll) does not call set_restart_fn; it returns poll_select_finish(...) | never restarted |
semop / semtimedop | (not separately measured) | no restart code at all | ipc/sem.c contains no ERESTART* at v6.12; the sleep loop stores and returns plain -EINTR | never restarted |
socket read/write, SO_RCVTIMEO set | (not separately measured) | no restart code at all | sock_intr_errno() returns plain -EINTR | never restarted |
| socket read/write, no timeout | (not separately measured) | ERESTARTSYS | sock_intr_errno()’s MAX_SCHEDULE_TIMEOUT branch | restartable with SA_RESTART |
fork / clone / vfork with a signal already pending | (not observable — never reaches userspace) | ERESTARTNOINTR | copy_process() in kernel/fork.c | n/a — always restarted |
Per-syscall restart codes at Linux 6.12, measured on a 7.1.13 kernel. What it shows: signal(7)‘s “never restarted” list is not one rule but three different mechanisms: ERESTARTNOHAND (select, pause, absolute clock_nanosleep), ERESTART_RESTARTBLOCK (poll, relative nanosleep, timed futex), and plain -EINTR with no restart machinery involved at all (semop, timed sockets). The insight to take: two calls that look interchangeable can sit in different rows — poll returns 516 while select returns 514, and futex(FUTEX_WAIT) changes from 512 to 516 purely by being given a timeout. The behaviour is identical for a caught signal (all give EINTR), but it diverges the moment no handler runs: the 516 rows resume with a corrected deadline, the 514 rows re-issue with whatever arguments are still in the registers, and the plain-EINTR rows do not resume at all.
Why
selectcan get away with a plain re-issue
ERESTARTNOHANDre-issues the original syscall with the original register arguments — which, for a call with a timeout, would normally restart the clock.selectgets away with it because on Linux theselectsyscall overwrites its owntimevalargument in userspace memory with the remaining time before returning.poll_select_finish()does that write, and only if it fails does the code convert-ERESTARTNOHANDinto-EINTR, with a comment that states the dependency outright: “because we’re not updating the timeval, we can’t restart the system call” (fs/select.c, v6.12). The two triggers for that fallback are a faulting write (a read-onlytimeval) and theSTICKY_TIMEOUTSpersonality bit. So the argument-in-memory is the restart state forselect, playing the rolerestart_blockplays forpoll— and the measurement below confirms it: after an interrupted 1-secondselect, the caller’stvreads0.7999s. This is also why the glibcpselect/ppollwrappers pass a local copy of the timeout to the kernel: the syscall modifies it, and POSIX says it must not.
The restart_block Mechanism for Timeout Calls
Why the special treatment for ERESTART_RESTARTBLOCK? Because a naive restart of a timeout-bearing call would be wrong. Consider nanosleep(10s) interrupted after 3 seconds: a plain re-issue of the same syscall would sleep a fresh 10 seconds, for 13 total. The correct behavior is to sleep only the remaining 7 seconds. But the original arguments in the registers say ”10s” — they cannot carry the adjusted deadline. The restart_block solves this by saving the absolute deadline in per-task state and routing the restart through a generic restart_syscall() that reads it back.
Each task_struct carries one struct restart_block (include/linux/restart_block.h):
struct restart_block {
unsigned long arch_data;
long (*fn)(struct restart_block *);
union {
struct { /* ... */ } futex;
struct {
clockid_t clockid;
enum timespec_type type;
union {
struct __kernel_timespec __user *rmtp;
struct old_timespec32 __user *compat_rmtp;
};
u64 expires; /* absolute deadline */
} nanosleep;
struct { /* ... */ } poll;
};
};fnis the continuation — the function to call to resume the operation.- The union holds the per-call saved state. For
nanosleep,expiresis the absolute deadline; forfutex, the address and bitset; forpoll, the file-descriptor set and remaining timeout.
The restart_syscall() syscall itself is trivial — it just calls the saved continuation (kernel/signal.c):
SYSCALL_DEFINE0(restart_syscall)
{
struct restart_block *restart = ¤t->restart_block;
return restart->fn(restart);
}A call that does not want restart sets fn = do_no_restart_syscall, which simply returns -EINTR — a defensive default so a stale restart_block cannot accidentally re-run something. The restart_syscall(2) man page confirms there is no glibc wrapper and that it “is intended for use only by the kernel and should never be called by applications.”
Worked example: nanosleep
The hrtimer sleep path (kernel/time/hrtimer.c) is the canonical user of this mechanism:
long hrtimer_nanosleep(ktime_t rqtp, const enum hrtimer_mode mode,
const clockid_t clockid)
{
struct restart_block *restart;
struct hrtimer_sleeper t;
int ret = 0;
hrtimer_init_sleeper_on_stack(&t, clockid, mode);
hrtimer_set_expires_range_ns(&t.timer, rqtp, current->timer_slack_ns);
ret = do_nanosleep(&t, mode);
if (ret != -ERESTART_RESTARTBLOCK)
goto out;
/* Absolute timers do not update the rmtp value and restart: */
if (mode == HRTIMER_MODE_ABS) {
ret = -ERESTARTNOHAND;
goto out;
}
restart = ¤t->restart_block;
restart->nanosleep.clockid = t.timer.base->clockid;
restart->nanosleep.expires = hrtimer_get_expires_tv64(&t.timer);
set_restart_fn(restart, hrtimer_nanosleep_restart);
out:
destroy_hrtimer_on_stack(&t.timer);
return ret;
}When do_nanosleep() is interrupted, it returns -ERESTART_RESTARTBLOCK. hrtimer_nanosleep() then records the absolute expiry (expires) and the clock into the nanosleep arm of the union, and calls set_restart_fn() to wire up the continuation. set_restart_fn() (include/linux/thread_info.h) is itself instructive:
static inline long set_restart_fn(struct restart_block *restart,
long (*fn)(struct restart_block *))
{
restart->fn = fn;
arch_set_restart_data(restart);
return -ERESTART_RESTARTBLOCK;
}It stores the continuation, lets the architecture stash any extra data (arch_set_restart_data records the compat/x32 mode in arch_data on x86), and returns the code — a tidy idiom so callers return set_restart_fn(...). The continuation re-arms an absolute timer at the saved deadline:
static long __sched hrtimer_nanosleep_restart(struct restart_block *restart)
{
struct hrtimer_sleeper t;
int ret;
hrtimer_init_sleeper_on_stack(&t, restart->nanosleep.clockid,
HRTIMER_MODE_ABS);
hrtimer_set_expires_tv64(&t.timer, restart->nanosleep.expires);
ret = do_nanosleep(&t, HRTIMER_MODE_ABS);
/* ... */
return ret;
}Because the deadline is absolute, restarting “sleeps until time T” rather than “sleeps for N more nanoseconds,” so elapsed time is accounted for automatically — even across multiple interruptions. This is precisely what restart_syscall(2) means by “a time argument that is suitably adjusted to account for the time that has already elapsed.”
Correction to an earlier draft of this note
An earlier revision listed “the System V semaphore wait” among the
restart_blockusers. That is wrong, and grepping the file settles it:ipc/sem.cat v6.12 contains no occurrence of anyERESTART*symbol at all.semtimedop()’s sleep loop writes-EINTRintoqueue.statusdirectly and returns it — sosemop/semtimedopare “never restarted” for a third reason, namely that they never enter the restart machinery.io_geteventswas also listed without verification and is not confirmed here.
Who actually uses restart_block
The union in struct restart_block has exactly three arms at v6.12 — futex, nanosleep, and poll — but the fn pointer is generic, so more subsystems ride the mechanism using one of those arms or none at all.
| User | Arm of the union | Continuation (fn) | What is saved | Why a plain re-issue would be wrong |
|---|---|---|---|---|
nanosleep, relative clock_nanosleep | nanosleep | hrtimer_nanosleep_restart | clockid, expires (absolute), type, rmtp | the register argument says “10 s”; re-issuing would sleep 10 s again |
poll, and do_restart_poll re-arming itself | poll | do_restart_poll | ufds, nfds, has_timeout, tv_sec/tv_nsec of the absolute end time | same — the timeout_msecs argument is relative |
futex(FUTEX_WAIT) with a timeout | futex | futex_wait_restart | uaddr, val, bitset, flags | FLAGS_HAS_TIMEOUT, time (absolute) | same, plus the futex value/bitset must be re-checked against the original expectation |
alarmtimer (alarm_timer_nsleep) | nanosleep | alarm_timer_nsleep_restart | as nanosleep | same |
posix-cpu-timers (posix_cpu_nsleep) | nanosleep | posix_cpu_nsleep_restart | as nanosleep, on a CPU clock | same |
| any call that does not want a restart | — | do_no_restart_syscall | nothing | it is the defensive default: it returns -EINTR unconditionally |
The restart_block clients at v6.12 (the last three confirmed from the file list of commit 5abbe51a5262, which converted every restart->fn = … assignment in the tree to set_restart_fn()). What it shows: every client is a call with a deadline, and every one of them saves that deadline in absolute form. The insight to take: restart_block is not a general “resume anything” facility — it exists because relative timeouts are the one class of syscall argument that becomes stale the instant the call is interrupted. Arguments that are still valid after an interruption (a file descriptor, a buffer pointer, a futex address with no timeout) need no saved state at all, which is why those calls use the cheaper ERESTARTSYS/ERESTARTNOHAND re-issue instead.
sequenceDiagram autonumber participant U as User code participant K as nanosleep handler<br/>(hrtimer_nanosleep) participant RB as current->restart_block participant S as arch_do_signal_or_restart participant RS as sys_restart_syscall U->>K: syscall nr=35 (nanosleep), req = 500 ms Note over K: t0. deadline := now + 500 ms<br/>arms an hrtimer, sleeps Note over K: t0+200 ms: SIGSTOP arrives,<br/>signal_pending() → sleeper wakes K->>RB: nanosleep.clockid = CLOCK_MONOTONIC<br/>nanosleep.expires = t0 + 500 ms (ABSOLUTE)<br/>fn = hrtimer_nanosleep_restart K->>U: return -ERESTART_RESTARTBLOCK (-516) Note over U: (user code never runs — we are<br/>still inside the exit path) U->>S: exit_to_user_mode_loop → TIF_SIGPENDING Note over S: get_signal() handles the stop<br/>internally and returns FALSE S->>S: ax = __NR_restart_syscall (219)<br/>ip -= 2 Note over U: task is STOPPED until t0+1500 ms,<br/>then SIGCONT resumes it U->>RS: the same 2 bytes re-execute, now as nr=219 RS->>RB: restart->fn(restart) RB->>K: hrtimer_nanosleep_restart():<br/>HRTIMER_MODE_ABS at t0+500 ms Note over K: that deadline is already in the past<br/>→ returns immediately K->>U: return 0 (success)
A relative nanosleep(500 ms) that is stopped at t0+200 ms and resumed at t0+1500 ms. What it shows: the syscall number in RAX is replaced — the re-executed syscall instruction at the same address invokes restart_syscall (219), not nanosleep (35) — and the only thing carrying the original intent across that swap is the absolute expires field in current->restart_block. The insight to take: this is the one place in the whole restart mechanism where the restarted call is not the call the program made. The absolute deadline is what makes the answer correct: because expires is a point in time and not a duration, the 1.3 seconds the task spent stopped are accounted for for free, and the measurement below shows exactly this — the call returns 0 after 1.500 s of wall time, not after 2.000 s as a naive re-issue would give.
Why the Two Man-Page Lists Are What They Are
signal(7) publishes two lists: calls “automatically restarted after the signal handler returns if the SA_RESTART flag was used,” and calls that “are never restarted (regardless of the use of SA_RESTART)” — the latter including nanosleep, clock_nanosleep, poll, select, epoll_wait, sigtimedwait, System V semop, and the socket calls with a timeout set. These lists are not arbitrary policy; they fall directly out of the code above:
- The first list is exactly the set of calls whose blocking primitive returns
-ERESTARTSYS. Perhandle_signal, those restart iffSA_RESTART. - The second list is mostly calls that return
-ERESTART_RESTARTBLOCK. Perhandle_signal, that code is converted to-EINTRwhenever a handler runs — and theSA_RESTARTbit is never tested for it. So no matter what flags you set, if you have a handler for the delivered signal, these calls returnEINTR.
This is the load-bearing connection between the kernel mechanism and the userspace-visible behavior described in EINTR and Interrupted System Calls. The man page’s “never restarted with SA_RESTART” is a consequence of ERESTART_RESTARTBLOCK hitting the regs->ax = -EINTR branch before the SA_RESTART check, not an independent rule.
Resolved — this note previously flagged the per-call mapping as unverified
An earlier revision carried an
#uncertaincallout here asking whetherselect/pselectreturnERESTARTNOHANDrather thanERESTART_RESTARTBLOCK, and where socketSO_RCVTIMEOtimeouts fit. Both are now settled, from source and from measurement (see the per-syscall table above and Demonstrating It below).selectandpselect6return-ERESTARTNOHANDfromcore_sys_select(); socket calls with a timeout do not return a restart code at all but plain-EINTRfromsock_intr_errno();semop/semtimedoplikewise return plain-EINTRwith no restart machinery. The correction the callout was hedging against was real: signal(7)‘s “never restarted” list is three mechanisms, not one.
So the second list is really three lists that happen to share an observable behaviour:
| Mechanism | Calls | What happens when a handler runs | What happens when no handler runs |
|---|---|---|---|
ERESTARTNOHAND (514) | select, pselect6, pause, sigsuspend, clock_nanosleep(TIMER_ABSTIME), ppoll | -EINTR | re-issued, arguments unchanged (safe because select/ppoll rewrote their own timeout in user memory, and TIMER_ABSTIME never needed adjusting) |
ERESTART_RESTARTBLOCK (516) | poll, relative nanosleep/clock_nanosleep, timed futex, alarmtimer, POSIX CPU-clock sleeps | -EINTR | re-issued as restart_syscall, with an absolute deadline recovered from current->restart_block |
plain -EINTR, no restart code | semop/semtimedop, socket calls with SO_RCVTIMEO/SO_SNDTIMEO set | -EINTR | -EINTR — the call simply fails; there is nothing for the restart machinery to do |
Three routes to the same “never restarted with SA_RESTART” line in signal(7). What it shows: the observable behaviour under a caught signal is identical across all three rows, which is why the man page can collapse them into one list — but the behaviour under a stop signal, where no handler runs, is completely different. The insight to take: if you are debugging why a call did or did not come back after a SIGSTOP/SIGCONT (or after a signal you had set to SIG_IGN), the man-page list will not help you; you need the row. That is the practical payoff of knowing the internal code, and it is why the gdb recipe below is worth having.
Stop signals: the fourth column signal(7) does print
signal(7) has a second, much less famous subsection — “Interruption of system calls and library functions by stop signals” — which notes that “even in the absence of signal handlers, certain blocking interfaces can fail with the error EINTR after the process is stopped by one of the stop signals and then resumed via SIGCONT. This behavior is not sanctioned by POSIX.1, and doesn’t occur on other systems.” The Linux interfaces it names are the timed socket calls, epoll_wait/epoll_pwait, semop/semtimedop, and sigtimedwait/sigwaitinfo.
Map that onto the table above and it stops being a mystery: every interface on the stop-signal list is one that returns plain -EINTR rather than a restart code. Because -EINTR is not an ERESTART* value, neither switch statement matches it, regs->ax is left alone, and the -4 sails out to userspace — even though get_signal() returned false and the “no handler” column of the truth table would have restarted any of the three restart codes. The stop-signal list is, precisely, the list of calls that opted out of the restart mechanism.
The seccomp user-notification escape hatch
There is one documented way for a call in the “never restarted” list to be restarted anyway, and it is worth knowing because it looks like a kernel bug when you hit it. seccomp_unotify(2) describes a target process blocked in a seccomp user-space notification while its supervisor decides what to do; if a signal handler runs in the target during that wait, “the kernel will restart the target’s system call” and the supervisor receives a second notification for the same call. The man page flags the consequence explicitly: “One oddity is that system call restarting as described in this scenario will occur even for the blocking system calls listed in signal(7) that would never normally be restarted by the SA_RESTART flag.”
The mechanism falls straight out of this note. In seccomp_do_user_notification() (kernel/seccomp.c, v6.12) the target waits with wait_for_completion_interruptible(&n.ready), whose documented return on interruption is -ERESTARTSYS. That value is returned from the syscall-entry work, before the real handler ever runs — so the syscall that gets an ERESTARTSYS is whatever call triggered the filter, poll and nanosleep included. Those calls never produce ERESTARTSYS themselves, but here they never got as far as their own implementation. Row 1 of the truth table then applies, and with SA_RESTART set the call transparently restarts. Nothing special-cased it; the notification wait simply substituted its own restart verb for the one the call would have used. (Ownership note: Seccomp and seccomp-BPF and seccomp and Syscall Filtering own the notification protocol itself; this note owns only the restart consequence.)
The ptrace Observability Window
The errno header notes one legitimate observer of these codes: “ptrace can observe these at syscall exit tracing, but they will never be left for the debugged user process to see” (errno.h, v6.12). A tracer stopped at the syscall-exit trap (see ptrace and Syscall Tracing) can read -ERESTARTSYS straight out of pt_regs, because the trap fires before arch_do_signal_or_restart() resolves it. This is why strace can sometimes print = ? ERESTARTSYS (To be restarted ...) — it is peeking at the internal code through the ptrace window. The traced process itself still only ever resumes the call or receives EINTR.
Demonstrating It
Everything above is directly observable. All measurements below were taken on Fedora 44, kernel 7.1.13-200.fc44.x86_64, glibc 2.43-8.fc44, GCC 16.2.1, GDB 17.2, on x86-64. (strace and ltrace are not installed on this machine, so the raw-register observations use gdb -batch with catch syscall instead — the recipe is given in full below and is a strictly better tool for this particular job, because it lets you read orig_ax and rip as well as rax.)
The truth table, executed
The harness installs a SIGUSR1 handler with and without SA_RESTART, forks a helper that pokes the parent partway through a blocking call, and times the result. The signal always arrives; the only variable is the flag.
== Path A: a handler runs (get_signal() -> true -> handle_signal) ==
-- ERESTARTSYS: SA_RESTART decides --
read(pipe) SA_RESTART=0 -> ret=-1 errno=EINTR (Interrupted system call) elapsed=0.150s handler_hits=1
read(pipe) SA_RESTART=1 -> ret=1 errno=- (ok) elapsed=0.500s handler_hits=1
-- ERESTART_RESTARTBLOCK / ERESTARTNOHAND: SA_RESTART is never consulted --
nanosleep(1s) SA_RESTART=0 -> ret=-1 errno=EINTR elapsed=0.300s rem=0.699965422s hits=1
nanosleep(1s) SA_RESTART=1 -> ret=-1 errno=EINTR elapsed=0.300s rem=0.699959711s hits=1
poll(1000ms) SA_RESTART=0 -> ret=-1 errno=EINTR elapsed=0.200s hits=1
poll(1000ms) SA_RESTART=1 -> ret=-1 errno=EINTR elapsed=0.200s hits=1
select(1s) SA_RESTART=0 -> ret=-1 errno=EINTR elapsed=0.200s tv_left=0.799895s hits=1
select(1s) SA_RESTART=1 -> ret=-1 errno=EINTR elapsed=0.200s tv_left=0.799927s hits=1
pause() SA_RESTART=0 -> ret=-1 errno=EINTR elapsed=0.200s hits=1
pause() SA_RESTART=1 -> ret=-1 errno=EINTR elapsed=0.200s hits=1
== Path B: no handler runs (get_signal() -> false) ==
read(pipe) stopped+continued (NO handler) -> ret=1 errno=- elapsed=0.900s
nanosleep(500ms) stopped 200ms..1500ms (NO handler) -> ret=0 errno=- elapsed=1.500s rem=0.299960819s
pause() stopped 200ms..600ms then SIGWINCH@1200ms -> ret=-1 errno=EINTR elapsed=1.200s
Read the numbers against the truth table, one row at a time:
readon a pipe,SA_RESTART=0— the writer child was going to write at 500 ms; the signal arrived at 150 ms;readreturnedEINTRat 0.150 s. Row 2 of the table.readon a pipe,SA_RESTART=1— same signal at the same moment, butreadreturned1at 0.500 s, exactly when the writer wrote, withhandler_hits=1proving the handler did run. The syscall was re-issued underneath the program. Row 1.nanosleep,poll,select,pause— all four returnedEINTRat the moment of the signal, identically with and withoutSA_RESTART. The flag changed nothing, because none of these calls returnsERESTARTSYS. Rows 7–12.select’stv_left = 0.7999 s— the caller’sstruct timevalwas rewritten by the kernel with the remaining time. This is the Linux-specific behaviour that makes a plainERESTARTNOHANDre-issue correct forselect, and it is visible from userspace.nanosleep’srem = 0.6999 s— the classic “resume the sleep yourself” value, which is what a userspace retry loop must feed back into the nextnanosleep.readstopped and continued, no handler —SIGSTOPat 150 ms,SIGCONTat 500 ms, writer at 900 ms.readreturned1at 0.900 s: it restarted through the “no handler” column and the program never saw an error. This is the row people forget exists, and it is why “I never installed a handler so myreadis safe” is true for a different reason than they think.nanosleep(500 ms)stopped from 200 ms to 1500 ms, no handler — returned0at 1.500 s. This is therestart_blockproof. The absolute deadlinet0 + 500 mshad already passed whenSIGCONTarrived, sohrtimer_nanosleep_restart()returned immediately. A naive re-issue ofnanosleep(500 ms)would have slept a fresh half-second and finished at 2.000 s. The 500 ms difference is the mechanism, measured.pause()stopped from 200 ms to 600 ms — did not return at 600 ms. It kept waiting until a genuine caught signal (SIGWINCHat 1200 ms) arrived, and only then returnedEINTRat 1.200 s.ERESTARTNOHANDunder the no-handler column restarts; under the handler column it does not. Rows 9 and 7.
One genuine oddity in that output deserves its own note, because it surprised me and it is not documented anywhere I could find:
nanosleep'sremcan be clobbered on a successful callLook at the Path-B
nanosleepline: it returned0(success) and yetremreads0.2999 s, a value that describes an intermediate state rather than the final one. The cause is indo_nanosleep()(kernel/time/hrtimer.c, v6.12): when the sleep is interrupted it callsnanosleep_copyout(restart, &rmt)— which writes the remaining time into the caller’srmtp— before returning-ERESTART_RESTARTBLOCK. If the call then goes on to restart and succeed, that write is never undone. POSIX only definesremwhen the call fails withEINTR, so this is not a standards violation, but code that readsremunconditionally will read a stale value. Treatremas meaningful only whennanosleepreturned-1witherrno == EINTR.
Reading the raw internal code through the ptrace window
This is the measurement that produced the per-syscall table. The program blocks in exactly one syscall with a handler installed without SA_RESTART; gdb stops at the syscall-exit trap, which fires inside syscall_exit_to_user_mode() before arch_do_signal_or_restart() runs, so RAX still holds the unresolved internal code.
The portable recipe — useful anywhere strace is unavailable, and strictly more informative than strace here because it exposes orig_ax and rip too:
set confirm off
set pagination off
set follow-fork-mode parent
set detach-on-fork on
handle SIGUSR1 nostop noprint pass # let the signal through without stopping
tbreak blocking_call # skip libc startup, which also does syscalls
run
catch syscall nanosleep # arm only after we are in our own code
continue
printf " entry trap: rax=%ld orig_rax=%ld\n", $rax, $orig_rax
continue
printf " EXIT trap: rax=%ld orig_rax=%ld rip=%#lx\n", $rax, $orig_rax, $rip
x/2xb $rip-2
continueThree of the lines are load-bearing and easy to get wrong. handle SIGUSR1 nostop noprint pass is mandatory: by default gdb intercepts the signal, so the inferior never reaches get_signal() and nothing is ever restarted. tbreak before catch syscall matters because glibc’s startup issues plenty of syscalls of the same name (an unqualified catch syscall read fires first on the loader reading the binary). And x/2xb $rip-2 prints the instruction the restart logic rewinds onto.
Output, one run per syscall (abridged to the two printf lines plus the byte dump):
---------- read (catch syscall read) ----------
entry trap: rax=-38 orig_rax=0
EXIT trap: rax=-512 orig_rax=0 rip=0x7ffff7e9437d
0x7ffff7e9437b <syscall+27>: 0x0f 0x05
read libc-visible ret=-1 errno=4 (Interrupted system call)
---------- nanosleep ----------
entry trap: rax=-38 orig_rax=35
EXIT trap: rax=-516 orig_rax=35 rip=0x7ffff7e9437d
---------- poll ----------
entry trap: rax=-38 orig_rax=7
EXIT trap: rax=-516 orig_rax=7 rip=0x7ffff7e9437d
---------- select ----------
entry trap: rax=-38 orig_rax=23
EXIT trap: rax=-514 orig_rax=23 rip=0x7ffff7e9437d
---------- pause ----------
entry trap: rax=-38 orig_rax=34
EXIT trap: rax=-514 orig_rax=34 rip=0x7ffff7e9437d
---------- futex (FUTEX_WAIT, timeout = NULL) ----------
entry trap: rax=-38 orig_rax=202
EXIT trap: rax=-512 orig_rax=202 rip=0x7ffff7e9437d
---------- futex_to (FUTEX_WAIT, timeout = 5 s) ----------
entry trap: rax=-38 orig_rax=202
EXIT trap: rax=-516 orig_rax=202 rip=0x7ffff7e9437d
---------- wait4 ----------
entry trap: rax=-38 orig_rax=61
EXIT trap: rax=-512 orig_rax=61 rip=0x7ffff7e9437d
---------- clock_nanosleep (TIMER_ABSTIME) ----------
entry trap: rax=-38 orig_rax=230
EXIT trap: rax=-514 orig_rax=230 rip=0x7ffff7e9437d
Five separate claims from earlier in this note fall out of those twenty lines:
- The internal codes are real and observable.
-512,-514,-516appear verbatim inRAX. Every one of them turns into a libc-visibleerrno == 4(EINTR) by the time the program itself runs — the last line of thereadblock shows the conversion. orig_axholds the syscall number whileaxholds the error.nanosleepshowsorig_rax=35andrax=-516simultaneously. That is precisely the stateregs->ax = regs->orig_axrestores from.- The rewind target really is a two-byte instruction.
x/2xb $rip-2prints0x0f 0x05— thesyscallopcode — at every single stop.regs->ip -= 2lands exactly on it. - The per-syscall mapping is as the source says.
poll→ 516 butselect→ 514;futexflips from 512 to 516 purely by acquiring a timeout;clock_nanosleep(TIMER_ABSTIME)→ 514, matching themode == HRTIMER_MODE_ABSdowngrade branch inhrtimer_nanosleep()exactly. RAXis pre-loaded with-ENOSYSat entry. Every entry-trap line readsrax=-38. That is not the handler’s doing —entry_SYSCALL_64pushes it as part of the register frame (PUSH_AND_CLEAR_REGS rax=$-ENOSYS,arch/x86/entry/entry_64.S, v6.12), so a tracer that cancels the syscall by settingorig_ax = -1getsENOSYSfor free. The errno Convention and Negative Return Values covers why that particular value.
What this does not show
ERESTARTNOINTR (513) cannot be produced by this harness, and in fact cannot be observed from userspace at all by design: its only in-tree producer at v6.12 is copy_process(), and its meaning is “restart unconditionally”, so fork() never returns it to anyone. Confirming it requires reading kernel/fork.c rather than running anything.
Uncertain
Verify by measurement: the rows of the per-syscall table marked “not separately measured” —
ppoll,semop/semtimedop, and socket read/write with and withoutSO_RCVTIMEO. Reason: each of those claims is read from source (SYSCALL_DEFINE5(ppoll)not callingset_restart_fn;ipc/sem.ccontaining noERESTART*symbol at all;sock_intr_errno()’sMAX_SCHEDULE_TIMEOUTternary) but was not run through the gdb harness the way the other nine were, so a wrapper-level conversion I did not notice could still change the observed value. To resolve: extend thepeek2.charness with appollcase, asemtimedopcase on a System V semaphore, and a pair of socket cases differing only insetsockopt(SO_RCVTIMEO), and read$raxat the exit trap for each. I would expect-514, no restart code (plain-4),-512, and plain-4respectively. uncertainAlso unverified: which specific blocking primitive each of
epoll_wait,sigtimedwait,msgrcv/msgsnd,io_getevents,mq_receiveandgetrandomuses. signal(7) places them confidently on one list or the other, but I traced none of them to a return statement, so their rows are absent from the table above rather than guessed at.
Design History — Why It Looks Like This
The restart machinery is a 1980s BSD decision that Linux inherited and then had to keep patching. Neil Brown’s LWN series Ghosts of Unix past names signal delivery as its first example of an unfixable design, and the restart semantics specifically as one of the reasons: the Berkeley developers, having introduced sigvec() to make signals reliable, “also changed the semantics of some system calls so that they would restart rather than abort if a signal arrived while the system call was active” (Brown 2010). Brown enumerates the option space the way the kernel still sees it today — “Options include waiting for the system call to complete, aborting it completely, allowing it to return partial results, or allowing it to restart after the signal has been handled. Each of these can be the right answer in different contexts” — and observes that sigvec(), and later System V’s sigaction(), existed largely to let the programmer choose among them. His verdict on the result is blunt: “the exact semantics of interrupting a system call was hard to get right.” The four ERESTART* codes are Linux’s encoding of that option space, and the fact that there are four rather than one is the cost of “each of these can be the right answer.”
The kernel’s own comments carry the archaeology too. fs/select.c has preserved, unchanged for decades, a note explaining why select is ERESTARTNOHAND rather than ERESTARTSYS:
/*
* We can actually return ERESTARTSYS instead of EINTR, but I'd
* like to be certain this leads to no problems. So I return
* EINTR just for safety.
*
* Update: ERESTARTSYS breaks at least the xview clock binary, so
* I'm trying ERESTARTNOHAND which restart only when you want to.
*/That is the “we do not break userspace” rule operating in 1990s miniature: a more transparent restart was tried, one existing binary (the XView desktop clock) misbehaved, and the weaker code was adopted permanently. The comment appears twice in the file at v6.12 — once above core_sys_select() and once above compat_core_sys_select() — and is the direct reason select sits in signal(7)‘s “never restarted” list today.
timeline title How the restart machinery accumulated, with the evidence for each step 4.2BSD era : sigvec() adds per-signal blocking : "some system calls ... would restart rather than abort" (Brown, LWN 2010) System V : sigaction() adds flag bits : SA_RESTART becomes the programmer-visible switch early Linux : ERESTARTSYS tried for select() : reverted — "ERESTARTSYS breaks at least the xview clock binary" (fs/select.c comment, still present at v6.12) early Linux : ERESTARTNOHAND introduced : "which restart only when you want to" — same comment 2.6.x : restart_block + sys_restart_syscall : relative timeouts get an absolute deadline they can resume from 2016 (v4.5) : 95d97adb2bb8 : get_nr_restart_syscall() switched from TIF_IA32 to TS_COMPAT 2021 (v5.12-rc4) : 5abbe51a5262 + 8c150ba2fb59 : compat bit moved into restart_block.arch_data after a 32-bit sleep() restarted as madvise() under a 64-bit debugger 2026 (v6.12 / v6.18) : unchanged : the four codes and both switch statements are identical across the two LTS trees
The accretion of the restart mechanism, each entry tied to a source that was actually read for this note. What it shows: nothing here was designed in one go — ERESTARTNOHAND exists because ERESTARTSYS broke a desktop clock, restart_block exists because relative timeouts could not survive a re-issue, and arch_data exists because a debugger could make a 32-bit sleep restart as the wrong syscall entirely. The insight to take: the shape of the truth table is archaeological, not principled. That is the strongest argument for reading the two switch statements rather than reasoning about what the semantics “should” be.
The most recent structural change is from 2021 and is a small masterclass in how subtle this code is. Commit 5abbe51a5262 (Oleg Nesterov, merged for v5.12-rc4) introduced set_restart_fn() and arch_set_restart_data(), converting every restart->fn = …; return -ERESTART_RESTARTBLOCK; pair in fs/select.c, kernel/futex.c, kernel/time/alarmtimer.c, kernel/time/hrtimer.c and kernel/time/posix-cpu-timers.c into the single helper. The commit message calls it “Preparation for fixing get_nr_restart_syscall() on X86 for COMPAT” — and the bug it was preparing to fix, described in the companion commit 8c150ba2fb59, is worth reading in full:
Suppose that a 64bit debugger plays with a 32bit tracee and
- Tracee calls
sleep(2)//TS_COMPATis set- User interrupts the tracee by CTRL-C after 1 sec and does “(gdb) call func()”
- gdb saves the regs by
PTRACE_GETREGS… sets%rip='func'and%orig_rax=-1…PTRACE_CONT//TS_COMPATis cleared- func() hits int3 … Restore original regs by
PTRACE_SETREGS…PTRACE_CONT
get_nr_restart_syscall()wrongly returns__NR_restart_syscall==219, the tracee callsia32_sys_call_table[219] == sys_madvise.
A 32-bit sleep() that was interrupted, debugged, and resumed would restart as madvise(). The fix moved the “was this a compat syscall?” bit out of the volatile thread-info status field and into the restart_block itself, which survives the return to user mode — which is why struct restart_block carries an arch_data field and why arch_set_restart_data() on x86 is exactly restart->arch_data = current_thread_info()->status (arch/x86/include/asm/thread_info.h, v6.12). The get_nr_restart_syscall() quoted earlier in this note reads that saved bit back:
#ifdef CONFIG_IA32_EMULATION
if (current->restart_block.arch_data & TS_COMPAT)
return __NR_ia32_restart_syscall;
#endifThe moral for anyone reasoning about this code: the restart decision is taken after the syscall has finished and after a tracer may have rewritten the register frame, so every input it depends on must be stored somewhere a tracer cannot plausibly corrupt. restart_block is that place.
Failure Modes and Subtleties
-
A driver returns
-ERESTARTSYSwithout a pending signal. The header contract requiressignal_pending()to be set. If a buggyioctlor driver returnsERESTARTSYSwith no signal pending, neither resolution path triggers (no_TIF_SIGPENDING), and the raw512could leak toward userspace logic — a real class of driver bugs. The fix is always to gate the return onsignal_pending(current). -
ERESTARTNOINTRis rare and strong, and its real user isfork. It forces a restart even when a handler runs andSA_RESTARTis clear. An earlier revision of this note described its users vaguely as “the kernel-internal restart of certainsigreturn/setup paths”; grepping v6.12 settles it — the canonical producer iscopy_process()inkernel/fork.c, which does this immediately after collecting delayed multiprocess signals:spin_lock_irq(¤t->sighand->siglock); if (!(clone_flags & CLONE_THREAD)) hlist_add_head(&delayed.node, ¤t->signal->multiprocess); recalc_sigpending(); spin_unlock_irq(¤t->sighand->siglock); retval = -ERESTARTNOINTR; if (task_sigpending(current)) goto fork_out;The logic is exactly what the code name promises. If a signal is already pending when
fork()/clone()/vfork()starts, the kernel refuses to build a child that would inherit an ambiguous signal state; it bails out before any observable side effect and demands an unconditional retry. Because no child was created and nothing was published, restarting is not merely safe but mandatory — returningEINTRhere would makefork()spuriously fail for a program that merely had aSIGCHLDqueued. This also meansfork()is the one syscall for whichSA_RESTARTis irrelevant in both directions: it always restarts. The surrounding comment explains the ordering — “Force any signals received before this point to be delivered before the fork happens. Collect up signals sent to multiple processes that happen during the fork and delay them so that they appear to happen after the fork.” -
Returning
-EINTRwhere-ERESTARTSYSwas meant. The mirror of the above. A driver that returns-EINTRfrom an interruptible wait has hard-coded “never restart” into a call thatSA_RESTARTshould have covered; since-EINTR(4) matches neitherswitch, the value passes through untouched. This is the single most common way a well-behaved-looking driver ends up on the wrong side of the truth table, andmutex_lock_interruptible()’s-EINTRreturn is its most common source (see Where the ERESTART code is actually produced above). -
Assuming a restart means the syscall never started. It does not.
signal(7)is explicit for the I/O case: “If an I/O call on a slow device has already transferred some data by the time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).” A partially-completedread()returns a short count, notEINTRand not a restart, because by then it has a real result to report. A correct retry loop therefore has to handle three outcomes — full success, short count, andEINTR— not two. -
SA_RESTARTis per-sigaction, not per-process. The gatehandle_signal()reads isksig->ka.sa.sa_flags, the flags of the signal currently being delivered. Two signals arriving at the same blockedread()can resolve differently, and a library that installs a handler forSIGCHLDwithoutSA_RESTARTsilently makes every blocking call in the processEINTR-prone whenever a child exits — a failure mode raised in the LWN discussion of Brown’s article, where a commenter notes that “if, for example, you set a signal handler forSIGCHLDyou have major problems sinceSA_RESTARTcan’t be considered reliable (portably)” (comment thread). -
Stale
restart_block. Because there is a singlerestart_blockper task, a call that uses it must setfnevery time; thedo_no_restart_syscalldefault guards against a left-over continuation firing, but a call that forgets to reset state could in principle restart with wrong arguments. The kernel zeroes the relevant fields before each use (restart->fn = do_no_restart_syscallis set in thenanosleepentry wrappers). -
Absolute-timer calls do not restart via
restart_block. Note themode == HRTIMER_MODE_ABSbranch above converts to-ERESTARTNOHANDinstead — an absoluteclock_nanosleep(TIMER_ABSTIME)already names a fixed wall-clock target, so it can be restarted by plain re-issue (no time adjustment needed) and uses the simpler code.
See Also
- EINTR and Interrupted System Calls — the userspace-facing symptom this machinery produces; the retry-loop burden and the
EINTRvsEAGAINdistinction - SA_RESTART and Signal-Interrupted Syscalls — the per-handler
sigactionflag thathandle_signalreads to resolveERESTARTSYS - Returning to Userspace and exit_to_user_mode — the
exit_to_user_mode_loopthat callsarch_do_signal_or_restart - Thread Info Flags and Syscall Exit Work —
_TIF_SIGPENDING/_TIF_NOTIFY_SIGNALthat gate the signal path - The pt_regs Register Frame —
regs->ax,regs->orig_ax,regs->ipmanipulated by the restart logic - ptrace and Syscall Tracing — the one observer that can see
ERESTART*codes at the exit trap - The errno Convention and Negative Return Values — why
ERESTART*values sit above the userspace errno range, and why the header split makes their escape structurally impossible - The Generic Syscall Entry and Exit Layer — owns the exit work loop that calls
arch_do_signal_or_restart(); this note owns what happens inside it - libc Syscall Wrappers and errno Translation — the userspace side, which only ever sees the
-EINTRthis machinery produces - The x86-64 Syscall Calling Convention — why the
syscallinstruction is exactly the two bytes0F 05thatregs->ip -= 2rewinds onto - Seccomp and seccomp-BPF and seccomp and Syscall Filtering — the user-notification wait that can restart calls signal(7) says are never restarted
- signalfd — the fd-based alternative that sidesteps the whole restart question by never delivering a signal asynchronously
- Linux System Call Interface MOC — parent map (§6, Errors, Restart, and Interruption)
- Linux IPC MOC — signals are the trigger for every restart decision