Skip to content

Commit 736443c

Browse files
committed
os/exec: allow simultaneous cmd.Wait and Write of cmd.StdinPipe
cmd.StdinPipe returns an io.WriteCloser. It's reasonable to expect the caller not to call Write and Close simultaneously, but there is an implicit Close in cmd.Wait that's not obvious. We already synchronize the implicit Close in cmd.Wait against any explicit Close from the caller. Also synchronize that implicit Close against any explicit Write from the caller. Fixes #9307. Change-Id: I8561e9369d6e5ac88dfbca1175549f6dfa04b8ac Reviewed-on: https://go-review.googlesource.com/31148 Reviewed-by: Brad Fitzpatrick <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 5fbf35d commit 736443c

File tree

2 files changed

+66
-3
lines changed

2 files changed

+66
-3
lines changed

src/os/exec/exec.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,16 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
515515
c.Stdin = pr
516516
c.closeAfterStart = append(c.closeAfterStart, pr)
517517
wc := &closeOnce{File: pw}
518-
c.closeAfterWait = append(c.closeAfterWait, wc)
518+
c.closeAfterWait = append(c.closeAfterWait, closerFunc(wc.safeClose))
519519
return wc, nil
520520
}
521521

522522
type closeOnce struct {
523523
*os.File
524524

525-
once sync.Once
526-
err error
525+
writers sync.RWMutex // coordinate safeClose and Write
526+
once sync.Once
527+
err error
527528
}
528529

529530
func (c *closeOnce) Close() error {
@@ -535,6 +536,48 @@ func (c *closeOnce) close() {
535536
c.err = c.File.Close()
536537
}
537538

539+
type closerFunc func() error
540+
541+
func (f closerFunc) Close() error { return f() }
542+
543+
// safeClose closes c being careful not to race with any calls to c.Write.
544+
// See golang.org/issue/9307 and TestEchoFileRace in exec_test.go.
545+
// In theory other calls could also be excluded (by writing appropriate
546+
// wrappers like c.Write's implementation below), but since c is most
547+
// commonly used as a WriteCloser, Write is the main one to worry about.
548+
// See also #7970, for which this is a partial fix for this specific instance.
549+
// The idea is that we return a WriteCloser, and so the caller can be
550+
// relied upon not to call Write and Close simultaneously, but it's less
551+
// obvious that cmd.Wait calls Close and that the caller must not call
552+
// Write and cmd.Wait simultaneously. In fact that seems too onerous.
553+
// So we change the use of Close in cmd.Wait to use safeClose, which will
554+
// synchronize with any Write.
555+
//
556+
// It's important that we know this won't block forever waiting for the
557+
// operations being excluded. At the point where this is called,
558+
// the invoked command has exited and the parent copy of the read side
559+
// of the pipe has also been closed, so there should really be no read side
560+
// of the pipe left. Any active writes should return very shortly with an EPIPE,
561+
// making it reasonable to wait for them.
562+
// Technically it is possible that the child forked a sub-process or otherwise
563+
// handed off the read side of the pipe before exiting and the current holder
564+
// is not reading from the pipe, and the pipe is full, in which case the close here
565+
// might block waiting for the write to complete. That's probably OK.
566+
// It's a small enough problem to be outweighed by eliminating the race here.
567+
func (c *closeOnce) safeClose() error {
568+
c.writers.Lock()
569+
err := c.Close()
570+
c.writers.Unlock()
571+
return err
572+
}
573+
574+
func (c *closeOnce) Write(b []byte) (int, error) {
575+
c.writers.RLock()
576+
n, err := c.File.Write(b)
577+
c.writers.RUnlock()
578+
return n, err
579+
}
580+
538581
// StdoutPipe returns a pipe that will be connected to the command's
539582
// standard output when the command starts.
540583
//

src/os/exec/exec_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,26 @@ func TestCatStdin(t *testing.T) {
101101
}
102102
}
103103

104+
func TestEchoFileRace(t *testing.T) {
105+
cmd := helperCommand(t, "echo")
106+
stdin, err := cmd.StdinPipe()
107+
if err != nil {
108+
t.Fatalf("StdinPipe: %v", err)
109+
}
110+
if err := cmd.Start(); err != nil {
111+
t.Fatalf("Start: %v", err)
112+
}
113+
wrote := make(chan bool)
114+
go func() {
115+
defer close(wrote)
116+
fmt.Fprint(stdin, "echo\n")
117+
}()
118+
if err := cmd.Wait(); err != nil {
119+
t.Fatalf("Wait: %v", err)
120+
}
121+
<-wrote
122+
}
123+
104124
func TestCatGoodAndBadFile(t *testing.T) {
105125
// Testing combined output and error values.
106126
bs, err := helperCommand(t, "cat", "/bogus/file.foo", "exec_test.go").CombinedOutput()

0 commit comments

Comments
 (0)