Skip to content

os: write should handle EINTR #38033

@kolyshkin

Description

@kolyshkin

Go 1.14 release notes (https://golang.org/doc/go1.14#runtime) say

programs that use packages like syscall or golang.org/x/sys/unix will see more slow system calls fail with EINTR errors. Those programs will have to handle those errors in some way, most likely looping to try the system call again. For more information about this see man 7 signal for Linux systems or similar documentation for other systems.

This gives me an impression that unless I'm using syscall or golang.org/x/sys/unix directly, my code is fine.

From the signal(7) man page on Linux:

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;
<...>
read(2), readv(2), write(2), writev(2), and ioctl(2) calls <...>

Assuming that golang runtime always sets signal handlers with SA_RESTART, taken all the above into account, it seems right to conclude that using a high-level write function such as ioutils.WriteFile or os.Write should be fine.

Turns out it's not.

Originally reported in opencontainers/runc#2258

What version of Go are you using (go version)?

$ go1.14 version
go version go1.14 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go1.14 env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/kir/.cache/go-build"
GOENV="/home/kir/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/kir/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/kir/sdk/go1.14"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/kir/sdk/go1.14/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/kir/go/src/github.com/opencontainers/runc/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build157351814=/tmp/go-build -gno-record-gcc-switches"

What did you do?

The code below should be compiled (go test -c) and run as root on a modern Linux system.

package fscommon

import (
	"io/ioutil"
	"os"
	"path"
	"strconv"
	"testing"
)

const iter = 1000000

func TestWriteHandlesEINTR(t *testing.T) {
	memoryCgroupMount := "/sys/fs/cgroup/memory"
	cgroupPath, err := ioutil.TempDir(memoryCgroupMount, "test-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(cgroupPath)
	filename := path.Join(cgroupPath, "memory.limit_in_bytes")

	for i := 0; i < iter; i++ {
		limit := 1024*1024 + i

		f, err := os.OpenFile(filename, os.O_WRONLY, 0644)
		if err != nil {
			t.Fatal(err)
		}
		if n, err := f.WriteString(strconv.Itoa(limit)); err != nil {
			t.Fatalf("Failed to write %d on attempt %d (wrote %d bytes): %s", limit, i, n, err)
		}
		f.Close()
	}
}

func TestWriteFileHandlesEINTR(t *testing.T) {
	memoryCgroupMount := "/sys/fs/cgroup/memory"
	cgroupPath, err := ioutil.TempDir(memoryCgroupMount, "test-")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(cgroupPath)
	file := path.Join(cgroupPath, "memory.limit_in_bytes")

	for i := 0; i < iter; i++ {
		limit := 1024*1024 + i
		if err := ioutil.WriteFile(file, []byte(strconv.Itoa(limit)), 0644); err != nil {
			t.Fatalf("Failed to write %d on attempt %d: %s", limit, i, err)
		}
	}
}

What did you expect to see?

PASS

(the test passes with go 1.13.6 and probably all earlier versions)

What did you see instead?

[kir@kir-rhat fscommon]$ go1.14 test -c fscommon_test.go 
[kir@kir-rhat fscommon]$ sudo ./fscommon.test 
[sudo] password for kir: 
--- FAIL: TestWriteHandlesEINTR (0.10s)
    fscommon_test.go:30: Failed to write 1072354 on attempt 23778 (wrote 0 bytes): write /sys/fs/cgroup/memory/test-298000507/memory.limit_in_bytes: interrupted system call
--- FAIL: TestWriteFileHandlesEINTR (0.19s)
    fscommon_test.go:48: Failed to write 1094511 on attempt 45935: write /sys/fs/cgroup/memory/test-239836062/memory.limit_in_bytes: interrupted system call
FAIL

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.release-blocker

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions