-
Notifications
You must be signed in to change notification settings - Fork 18k
net: sendFile can't handle os.fileWithoutWriteTo #66988
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
Comments
Change https://go.dev/cl/581035 mentions this issue: |
Thank you for this report and catching this regression @philwo! Indeed restricting the reader to be an *os.File deprived other use cases IMHO. One way to ensure that the fix is for ever caught and doesn't trivially regress is by using a test such as one that I have crafted down below inside a new file net/sendfile_unix_alt_test.go // Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (darwin && !ios) || dragonfly || freebsd || solaris
package net
import (
"errors"
"io"
"io/fs"
"syscall"
"testing"
"time"
)
type sendFileMocker struct {
cursor int64
content []byte
seekInvoked bool
syscallConnInvoked bool
}
var _ fs.File = (*sendFileMocker)(nil)
var _ syscall.Conn = (*sendFileMocker)(nil)
func TestSendFileWorksOnNonOsFilesToo(t *testing.T) {
ln, err := Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
panic(err)
}
_, _ = io.Copy(io.Discard, conn)
}()
c, err := Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
defer c.Close()
sfm := &sendFileMocker{content: []byte("abcdefghijklmno")}
tcpConn := c.(*TCPConn)
_, err, _ = sendFile(tcpConn.fd, sfm)
if err == nil || err.Error() != "purposefully not available" {
t.Fatal("Purposefully expected .SyscallConn to return an error")
}
// After this invocation, sfm's variables must be set to true.
if !sfm.seekInvoked {
t.Error(".Seek was not invoked!")
}
if !sfm.syscallConnInvoked {
t.Error(".SyscallConn was not invoked!")
}
}
func (sfm *sendFileMocker) SyscallConn() (syscall.RawConn, error) {
sfm.syscallConnInvoked = true
return nil, errors.New("purposefully not available")
}
func (sfm *sendFileMocker) Seek(offset int64, whence int) (int64, error) {
sfm.seekInvoked = true
if whence == io.SeekEnd {
offset = -offset
} else if whence == io.SeekStart {
sfm.cursor = 0
}
sfm.cursor += offset
return sfm.cursor, nil
}
func (sfm *sendFileMocker) Size() int64 {
return int64(len(sfm.content))
}
func (sfm *sendFileMocker) Close() error {
return nil
}
func (sfm *sendFileMocker) Stat() (fs.FileInfo, error) {
return &mockFileInfo{sfm.Size()}, nil
}
type mockFileInfo struct{ size int64 }
var _ fs.FileInfo = (*mockFileInfo)(nil)
func (mfi *mockFileInfo) Name() string { return "in-memory" }
func (mfi *mockFileInfo) Size() int64 { return mfi.size }
func (mfi *mockFileInfo) IsDir() bool { return false }
func (mfi *mockFileInfo) Sys() any { return nil }
func (mfi *mockFileInfo) Mode() fs.FileMode { return 0 }
func (mfi *mockFileInfo) ModTime() time.Time { return time.Now() }
func (sfm *sendFileMocker) Read(b []byte) (int, error) {
n := copy(b, sfm.content[sfm.cursor:])
return n, nil
} |
Change https://go.dev/cl/581778 mentions this issue: |
The net package's sendfile tests exercise various paths where we expect sendfile to be used, but don't verify that sendfile was in fact used. Add a hook to internal/poll.SendFile to let us verify that sendfile was called when expected. Update os package tests (which use their own hook mechanism) to use this hook as well. For #66988 Change-Id: I7afb130dcfe0063d60c6ea0f8560cf8665ad5a81 Reviewed-on: https://go-review.googlesource.com/c/go/+/581778 Reviewed-by: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
Go version
go version go1.22.2 darwin/arm64
Output of
go env
in your module/workspace:What did you do?
f
for reading.s
.f
intos
by callingio.Copy(s, f)
Example: https://go.dev/play/p/xJbcSMVNLqy (deadlocks when run on Go Playground due to go.dev/issue/48394)
What did you see happen?
io.Copy
does not usesendfile
on macOS to copyf
intos
, but instead falls back to using a buffer due to a regression caused by a side-effect of go.dev/cl/472475.Stepping through the code with a debugger, I think this is what happens:
io.Copy
implements three different strategies to copysrc
to `dst:src.WriteTo(dst)
ifsrc
implements theWriterTo
interface.dst.ReadFrom(src)
ifdst
implements theReaderFrom
interface.In the first iteration, this goes through
os.File
'sWriteTo
method, which calls intozero_copy_stub.go
'swriteTo
, which is just a stub and thus can't do anything. It thus falls back to callingio.Copy
again with itself wrapped intofileWithoutWriteTo
, preventing it from taking the first branch.In this second iteration, it thus calls
ReadFrom
ons
and passes itf
wrapped intofileWithoutWriteTo
.tcpsock_posix.go
attempts to usesendFile
now, passingf
into itsr io.Reader
parameter.sendFile
needs to ensure that the passedReader
supports Stat, Seek and SyscallConn, and thus attempts a type assertion to*os.File
.However, this fails, because
r
isn't aFile
, it's afileWithoutWriteTo
.Prior to go.dev/cl/472475 this all happened to work, because we never took the first branch in
io.Copy
, becauseos.File
didn't implementio.WriterTo
, so we passed the unmodified arg to the socket'sReadFrom
.What did you expect to see?
io.Copy
should usesendfile
on macOS to copyf
intos
, like it did prior to go.dev/cl/472475 (and still does on Linux, becauseos/zero_copy_linux.go
handles it there and is not affected by the regression).The text was updated successfully, but these errors were encountered: