Skip to content

bufio: Reader.WriteTo makes an initial empty write #71424

@Luap99

Description

@Luap99

Go version

go version go1.24rc2 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3215251769=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/root/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/root/go'
GOPRIVATE=''
GOPROXY='direct'
GOROOT='/usr/lib/golang'
GOSUMDB='off'
GOTELEMETRY='local'
GOTELEMETRYDIR='/root/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/lib/golang/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24rc2'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

passing an bufio.Reader to io.Copy() results in an empty write() being issued. An empty write is normally not really an issue however in my case the writer is a unixpacket (SOCK_SEQPACKET) socket connection. In that case the empty write causes and empty read on the server. An an empty read() returns 0 which means the server misinterprets that message as EOF and closes the socket.

Consider this small example program, the program reads from stidn and writes that to the server running in the goroutine via io.Copy()

package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"os"
)

const socketName = "/tmp/testsock123"
const socketType = "unixpacket" // switch to "unix" and it works

func main() {
	buffered := flag.Bool("bufio", false, "use bufio")
	flag.Parse()

	os.Remove(socketName)
	socket, err := net.ListenUnix(socketType, &net.UnixAddr{Name: socketName, Net: socketType})
	if err != nil {
		log.Fatalln(err)
	}

	serverChan := make(chan struct{})
	// server routine
	go func() {
		conn, err := socket.Accept()
		if err != nil {
			log.Fatalln(err)
		}
		buf := make([]byte, 1024)
		for {
			i, err := conn.Read(buf)
			if err != nil {
				if err == io.EOF {
					fmt.Println("server got EOF")
					serverChan <- struct{}{}
					return
				}
				log.Fatalln(err)
			}
			fmt.Printf("read %d bytes, msg: %s\n", i, string(buf[:i]))
		}

	}()

	conn, err := net.DialUnix(socketType, nil, &net.UnixAddr{Name: socketName, Net: socketType})
	if err != nil {
		log.Fatalln(err)
	}

	var reader io.Reader = os.Stdin
	if *buffered {
		reader = bufio.NewReader(os.Stdin)
	}
	_, err = io.Copy(conn, reader)
	if err != nil {
		log.Fatalln(err)
	}
	conn.Close()
	<-serverChan
}

What did you see happen?

Only if the reader is wrapped in an bufio.Reader there is an extra empty write being made to the server. The server considers this to be an EOF and closes before the actual writes are made.
Not using bufio makes it work.

The example program:

$ go run main.go <<<"test"
read 5 bytes, msg: test

server got EOF

# using bufio makes it no longer work
$ go run main.go -bufio <<<"test"
server got EOF

strace clearly shows the behavior

...
write(4, "", 0)                         = 0
server got EOF
read(0, "test\n", 32768)                = 5
write(4, "test\n", 5)                   = 5
...

I guess for most cases the empty write does not matter (i.e. files or other stream based sockets) so most would not notice this but in case of SOCK_SEQPACKET/unixpacket it is important.

What did you expect to see?

Using bufio.Reader should not result in empty writes during io.Copy(). As far as I can tell this is because io.Copy() used the WriterTo() implementation here:
https://cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/bufio/bufio.go;l=518-521

So I think the bufio WriterTo() should be fixed to not cause empty write()'s. It should only have to write there if there is something in the buffer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.FrozenDueToAgeNeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions