Skip to content

runtime: signal handling: Go assumes SA_RESTART, but does not enforce it #20400

Closed
@bcmills

Description

@bcmills

The runtime registers all of its signal handlers with the SA_RESTART and SA_ONSTACK flags. It enforces that other handlers are registered with SA_ONSTACK, but does not enforce SA_RESTART.

Since the Go runtime does not register very many handlers when using -buildmode=c-archive and -buildmode=c-shared, other libraries that register handlers will not see an existing handler using SA_RESTART and thus will not know to propagate it. So in those programs, it is fairly likely that some handler will be registered without SA_RESTART.

Unfortunately, it appears that the Go standard library is written based on the assumption that all handlers use SA_RESTART. System calls in the standard library do not consistently check for the EINTR error and do not document its possibility. For example, many users would be surprised to learn that (*os.Cmd).CombinedOutput can return an os.SyscallError wrapping EINTR, as illustrated by the program below:

bcmills:~/src$ go version
go version devel +b53acd89db Tue May 16 17:15:11 2017 +0000 linux/amd64

eintr/eintr.go:

package main

import "C"

import (
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"syscall"
	"time"
)

//export go_annoy
func go_annoy(sig, seconds C.int) {
	annoy(syscall.Signal(sig), time.Duration(seconds)*time.Second)
}

func annoy(sig syscall.Signal, d time.Duration) {
	runtime.LockOSThread()
	pid := syscall.Getpid()
	tid := syscall.Gettid()
	exit := make(chan bool)
	go func() {
		for {
			select {
			case <-exit:
				return
			default:
			}
			if err := syscall.Tgkill(pid, tid, sig); err != nil {
				panic(err)
			}
		}
	}()

	started := time.Now()
	for time.Since(started) < d {
		cmd := exec.Command("/bin/echo", "Are we there yet?")
		_, err := cmd.CombinedOutput()
		if err != nil {
			fmt.Fprintln(os.Stderr, "exec.Cmd error: ", err)
			os.Exit(1)
		}
	}
	exit <- true
}

func main() {}

eintr/main/eintr.c:

#include <signal.h>
#include <stddef.h>
#include <string.h>

#include "eintr.h"

static void ignore_signal(int signo, siginfo_t *info, void *context) {
}

int main() {
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
  sa.sa_sigaction = ignore_signal;
  sigaction(SIGUSR1, &sa, NULL);

  go_annoy(SIGUSR1, 10);

  return 0;
}
bcmills:~/src$ go build -buildmode=c-archive eintr
bcmills:~/src$ $(go env CC) -pthread -static -I. eintr/main/eintr.c ./eintr.a -o eintr_main
bcmills:~/src$ ./eintr_main
exec.Cmd error:  fork/exec /bin/echo: interrupted system call
bcmills:~/src$ ./eintr_main
exec.Cmd error:  waitid: interrupted system call

In typical usage, the Go portion of the program would have only the exec.Command portion of the annoy function; the signal would originate externally (either from some other process, or perhaps from some other language runtime within the process).


I can think of three ways we could deal with this issue:

  1. Document that all signal handlers in a program containing a Go runtime must be registered with SA_RESTART and treat it the same way we do for SA_ONSTACK.
  2. Consistently check for EINTR throughout the standard library and transparently retry.
  3. Document that many Go functions can return EINTR and require end-users to check for it explicitly.

I believe that (3) is strictly worse than (2): if we can't handle EINTR correctly and consistently within the runtime, we can't reasonably expect users to do so.

I am not sure whether (1) is feasible. In particular, it might prevent the use of the Go runtime with many other languages (such languages that execute on a JVM).

That leaves us with (2). I am not familiar enough with syscall usage in the standard library to evaluate whether it is feasible.

(CC: @ianlancetaylor @mdempsky )

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions