-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
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.