Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

io.Pipe{Reader,Writer}.Close resets error code set with CloseWithError #17992

Closed
cyphar opened this issue Nov 20, 2016 · 2 comments
Closed

io.Pipe{Reader,Writer}.Close resets error code set with CloseWithError #17992

cyphar opened this issue Nov 20, 2016 · 2 comments

Comments

@cyphar
Copy link

cyphar commented Nov 20, 2016

% go version
go version go1.7 linux/amd64
% go env
GOARCH="amd64"
GOBIN="/home/cyphar/go/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/cyphar/.local"
GORACE=""
GOROOT="/usr/lib64/go"
GOTOOLDIR="/usr/lib64/go/pkg/tool/linux_amd64"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build573484999=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"

If you use io.Pipe you'll notice that calling .Close on the writer will un-do the error code set by .CloseWithError(err). I'm unsure if this is a bug, but I found this to be very unexpected. In particular when it's usual for you to use a defer writer.Close() code style. This is also available on play.golang.org: https://play.golang.org/p/zJDmmRTOLQ.

package main

import (
	"fmt"
	"io"
	"io/ioutil"
)

func main() {
	reader, writer := io.Pipe()

	go func() {
		defer writer.Close()

		fmt.Fprintf(writer, "some data to write before the error")
		writer.CloseWithError(fmt.Errorf("an error code"))
		fmt.Fprintf(writer, "some write that will fail")
	}()

	val, err := ioutil.ReadAll(reader)
	fmt.Printf("val: %s\n", val)
	fmt.Printf("err: %s\n", err)
}

If you comment out defer writer.Close() you see what I would expect.

What did you expect to see?

% go run pipe.go
val: some data to write before the error
err: an error code

What did you see instead?

% go run pipe.go
val: some data to write before the error
err: %!s(<nil>)

In particular, I did not expect for the error returned to change to EOF.

@cyphar
Copy link
Author

cyphar commented Nov 20, 2016

My proposed patch looks like this:

commit e781e43100151dbecdc6604d8c495ed7946e3979
Author: Aleksa Sarai <cyphar@cyphar.com>
Date:   Mon Nov 21 02:23:53 2016 +1100

    io: pipe: make CloseWithError final

    CloseWithError would previously overwrite previously set error flags,
    making a defer'd pipe.Close() set the error to EOF (clearing the
    previous error flags). This was quite confusing to users, because it
    meant that io.Pipes we're very different to channels (double-closing a
    pipe would not produce an error and would actually affect the pipe).

    Fixes: golang/go#17992
    Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>

diff --git a/src/io/pipe.go b/src/io/pipe.go
index 7e98cd2eb788..f1379a8278f0 100644
--- a/src/io/pipe.go
+++ b/src/io/pipe.go
@@ -93,26 +93,34 @@ func (p *pipe) write(b []byte) (n int, err error) {
        return
 }

-func (p *pipe) rclose(err error) {
+func (p *pipe) rclose(err error) error {
        if err == nil {
                err = ErrClosedPipe
        }
        p.l.Lock()
        defer p.l.Unlock()
+       if p.rerr != nil {
+               return ErrClosedPipe
+       }
        p.rerr = err
        p.rwait.Signal()
        p.wwait.Signal()
+       return nil
 }

-func (p *pipe) wclose(err error) {
+func (p *pipe) wclose(err error) error {
        if err == nil {
                err = EOF
        }
        p.l.Lock()
        defer p.l.Unlock()
+       if p.werr != nil {
+               return ErrClosedPipe
+       }
        p.werr = err
        p.rwait.Signal()
        p.wwait.Signal()
+       return nil
 }

 // A PipeReader is the read half of a pipe.
@@ -138,8 +146,7 @@ func (r *PipeReader) Close() error {
 // CloseWithError closes the reader; subsequent writes
 // to the write half of the pipe will return the error err.
 func (r *PipeReader) CloseWithError(err error) error {
-       r.p.rclose(err)
-       return nil
+       return r.p.rclose(err)
 }

 // A PipeWriter is the write half of a pipe.
@@ -168,8 +175,7 @@ func (w *PipeWriter) Close() error {
 //
 // CloseWithError always returns nil.
 func (w *PipeWriter) CloseWithError(err error) error {
-       w.p.wclose(err)
-       return nil
+       return w.p.wclose(err)
 }

 // Pipe creates a synchronous in-memory pipe.

cyphar added a commit to opencontainers/umoci that referenced this issue Nov 20, 2016
It turns out that .Close() will reset the error set by .CloseWithError()
which means that using defer will always cause errors to not be
propagated properly. To fix this, only call .Close* once (though this is
probably a Golang lib bug).

Fixes: cyphar/umoci#33
Reference: golang/go#17992
Signed-off-by: Aleksa Sarai <asarai@suse.com>
cyphar added a commit to opencontainers/umoci that referenced this issue Nov 20, 2016
It turns out that .Close() will reset the error set by .CloseWithError()
which means that using defer will always cause errors to not be
propagated properly. To fix this, only call .Close* once (though this is
probably a Golang lib bug).

Fixes: cyphar/umoci#33
Reference: golang/go#17992
Signed-off-by: Aleksa Sarai <asarai@suse.com>
@bradfitz
Copy link
Contributor

Double-Close isn't really defined either way, but reading the existing docs in the io package, it looks like the existing behavior matches what the docs say, even if it wasn't what you expected.

Changing it seems unwise. It seems just as likely to surprise people in the other direction and break existing code in the wild, so I'm inclined to not change behavior.

@golang golang locked and limited conversation to collaborators Nov 21, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants