Skip to content

Commit 58c73de

Browse files
os, runtime: better EPIPE behavior for command line programs
Old behavior: 10 consecutive EPIPE errors on any descriptor cause the program to exit with a SIGPIPE signal. New behavior: an EPIPE error on file descriptors 1 or 2 cause the program to raise a SIGPIPE signal. If os/signal.Notify was not used to catch SIGPIPE signals, this will cause the program to exit with SIGPIPE. An EPIPE error on a file descriptor other than 1 or 2 will simply be returned from Write. Fixes #11845. Update #9896. Change-Id: Ic85d77e386a8bb0255dc4be1e4b3f55875d10f18 Reviewed-on: https://go-review.googlesource.com/18151 Reviewed-by: Russ Cox <[email protected]>
1 parent a7d2b4d commit 58c73de

File tree

4 files changed

+134
-8
lines changed

4 files changed

+134
-8
lines changed

src/os/file_unix.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package os
88

99
import (
1010
"runtime"
11-
"sync/atomic"
1211
"syscall"
1312
)
1413

@@ -37,7 +36,6 @@ type file struct {
3736
fd int
3837
name string
3938
dirinfo *dirInfo // nil unless directory being read
40-
nepipe int32 // number of consecutive EPIPE in Write
4139
}
4240

4341
// Fd returns the integer Unix file descriptor referencing the open file.
@@ -67,13 +65,12 @@ type dirInfo struct {
6765
bufp int // location of next record in buf.
6866
}
6967

68+
// epipecheck raises SIGPIPE if we get an EPIPE error on standard
69+
// output or standard error. See the SIGPIPE docs in os/signal, and
70+
// issue 11845.
7071
func epipecheck(file *File, e error) {
71-
if e == syscall.EPIPE {
72-
if atomic.AddInt32(&file.nepipe, 1) >= 10 {
73-
sigpipe()
74-
}
75-
} else {
76-
atomic.StoreInt32(&file.nepipe, 0)
72+
if e == syscall.EPIPE && (file.fd == 1 || file.fd == 2) {
73+
sigpipe()
7774
}
7875
}
7976

src/os/pipe_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Test broken pipes on Unix systems.
6+
// +build !windows,!plan9,!nacl
7+
8+
package os_test
9+
10+
import (
11+
"fmt"
12+
"internal/testenv"
13+
"os"
14+
osexec "os/exec"
15+
"os/signal"
16+
"runtime"
17+
"syscall"
18+
"testing"
19+
)
20+
21+
func TestEPIPE(t *testing.T) {
22+
r, w, err := os.Pipe()
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
if err := r.Close(); err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
// Every time we write to the pipe we should get an EPIPE.
31+
for i := 0; i < 20; i++ {
32+
_, err = w.Write([]byte("hi"))
33+
if err == nil {
34+
t.Fatal("unexpected success of Write to broken pipe")
35+
}
36+
if pe, ok := err.(*os.PathError); ok {
37+
err = pe.Err
38+
}
39+
if se, ok := err.(*os.SyscallError); ok {
40+
err = se.Err
41+
}
42+
if err != syscall.EPIPE {
43+
t.Errorf("iteration %d: got %v, expected EPIPE", i, err)
44+
}
45+
}
46+
}
47+
48+
func TestStdPipe(t *testing.T) {
49+
testenv.MustHaveExec(t)
50+
r, w, err := os.Pipe()
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
if err := r.Close(); err != nil {
55+
t.Fatal(err)
56+
}
57+
// Invoke the test program to run the test and write to a closed pipe.
58+
// If sig is false:
59+
// writing to stdout or stderr should cause an immediate SIGPIPE;
60+
// writing to descriptor 3 should fail with EPIPE and then exit 0.
61+
// If sig is true:
62+
// all writes should fail with EPIPE and then exit 0.
63+
for _, sig := range []bool{false, true} {
64+
for dest := 1; dest < 4; dest++ {
65+
cmd := osexec.Command(os.Args[0], "-test.run", "TestStdPipeHelper")
66+
cmd.Stdout = w
67+
cmd.Stderr = w
68+
cmd.ExtraFiles = []*os.File{w}
69+
cmd.Env = append(os.Environ(), fmt.Sprintf("GO_TEST_STD_PIPE_HELPER=%d", dest))
70+
if sig {
71+
cmd.Env = append(cmd.Env, "GO_TEST_STD_PIPE_HELPER_SIGNAL=1")
72+
}
73+
if err := cmd.Run(); err == nil {
74+
if !sig && dest < 3 {
75+
t.Errorf("unexpected success of write to closed pipe %d sig %t in child", dest, sig)
76+
}
77+
} else if ee, ok := err.(*osexec.ExitError); !ok {
78+
t.Errorf("unexpected exec error type %T: %v", err, err)
79+
} else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok {
80+
t.Errorf("unexpected wait status type %T: %v", ee.Sys(), ee.Sys())
81+
} else if ws.Signaled() && ws.Signal() == syscall.SIGPIPE {
82+
if sig || dest > 2 {
83+
t.Errorf("unexpected SIGPIPE signal for descriptor %d sig %t", dest, sig)
84+
}
85+
} else {
86+
t.Errorf("unexpected exit status %v for descriptor %ds sig %t", err, dest, sig)
87+
}
88+
}
89+
}
90+
}
91+
92+
// This is a helper for TestStdPipe. It's not a test in itself.
93+
func TestStdPipeHelper(t *testing.T) {
94+
if os.Getenv("GO_TEST_STD_PIPE_HELPER_SIGNAL") != "" {
95+
signal.Notify(make(chan os.Signal, 1), syscall.SIGPIPE)
96+
}
97+
switch os.Getenv("GO_TEST_STD_PIPE_HELPER") {
98+
case "1":
99+
os.Stdout.Write([]byte("stdout"))
100+
case "2":
101+
os.Stderr.Write([]byte("stderr"))
102+
case "3":
103+
if _, err := os.NewFile(3, "3").Write([]byte("3")); err == nil {
104+
os.Exit(3)
105+
}
106+
default:
107+
t.Skip("skipping test helper")
108+
}
109+
// For stdout/stderr, we should have crashed with a broken pipe error.
110+
// The caller will be looking for that exit status,
111+
// so just exit normally here to cause a failure in the caller.
112+
// For descriptor 3, a normal exit is expected.
113+
os.Exit(0)
114+
}

src/os/signal/doc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ for a blocked signal, it will be unblocked. If, later, Reset is
8787
called for that signal, or Stop is called on all channels passed to
8888
Notify for that signal, the signal will once again be blocked.
8989
90+
SIGPIPE
91+
92+
When a Go program receives an EPIPE error from the kernel while
93+
writing to file descriptors 1 or 2 (standard output or standard
94+
error), it will raise a SIGPIPE signal. If the program is not
95+
currently receiving SIGPIPE via a call to Notify, this will cause the
96+
program to exit with SIGPIPE. On descriptors other than 1 or 2, the
97+
write will return the EPIPE error. This means that, by default,
98+
command line programs will behave like typical Unix command line
99+
programs, while other programs will not crash with SIGPIPE when
100+
writing to a closed network connection.
101+
90102
Go programs that use cgo or SWIG
91103
92104
In a Go program that includes non-Go code, typically C/C++ code

src/runtime/signal1_unix.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ func resetcpuprofiler(hz int32) {
139139
}
140140

141141
func sigpipe() {
142+
if sigsend(_SIGPIPE) {
143+
return
144+
}
142145
setsig(_SIGPIPE, _SIG_DFL, false)
143146
raise(_SIGPIPE)
144147
}

0 commit comments

Comments
 (0)