server: fix fd leak in DialTCP() on Linux

A new socket was created so we must close it before this function
returns _either_ on failure or success. On success, net.FileConn() in
newTCPConn() increases the refcount of the socket so this fi.Close()
doesn't destroy the socket; the caller can use it (and needs to close
it later).

Signed-off-by: FUJITA Tomonori <[email protected]>
This commit is contained in:
FUJITA Tomonori
2017-08-13 00:22:10 +09:00
parent 534531b1b1
commit f615fa49a7
2 changed files with 52 additions and 6 deletions
+12 -6
View File
@@ -177,6 +177,15 @@ func (d *TCPDialer) DialTCP(addr string, port int) (*net.TCPConn, error) {
if err != nil {
return nil, err
}
fi := os.NewFile(uintptr(fd), "")
defer fi.Close()
// A new socket was created so we must close it before this
// function returns either on failure or success. On success,
// net.FileConn() in newTCPConn() increases the refcount of
// the socket so this fi.Close() doesn't destroy the socket.
// The caller must call Close() with the file later.
// Note that the above os.NewFile() doesn't play with the
// refcount.
if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil {
return nil, os.NewSyscallError("setsockopt", err)
@@ -208,10 +217,7 @@ func (d *TCPDialer) DialTCP(addr string, port int) (*net.TCPConn, error) {
return nil, os.NewSyscallError("bind", err)
}
newTCPConn := func(fd int) (*net.TCPConn, error) {
fi := os.NewFile(uintptr(fd), "")
defer fi.Close()
newTCPConn := func(fi *os.File) (*net.TCPConn, error) {
if conn, err := net.FileConn(fi); err != nil {
return nil, err
} else {
@@ -224,7 +230,7 @@ func (d *TCPDialer) DialTCP(addr string, port int) (*net.TCPConn, error) {
case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
// do timeout handling
case nil, syscall.EISCONN:
return newTCPConn(fd)
return newTCPConn(fi)
default:
return nil, os.NewSyscallError("connect", err)
}
@@ -259,7 +265,7 @@ func (d *TCPDialer) DialTCP(addr string, port int) (*net.TCPConn, error) {
switch err := syscall.Errno(nerr); err {
case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
case syscall.Errno(0), syscall.EISCONN:
return newTCPConn(fd)
return newTCPConn(fi)
default:
return nil, os.NewSyscallError("getsockopt", err)
}
+40
View File
@@ -18,8 +18,12 @@ package server
import (
"bytes"
"fmt"
"net"
"os"
"syscall"
"testing"
"time"
"unsafe"
)
@@ -64,3 +68,39 @@ func Test_buildTcpMD5Sigv6(t *testing.T) {
t.Error("Something wrong v6")
}
}
func Test_DialTCP_FDleak(t *testing.T) {
openFds := func() int {
pid := os.Getpid()
f, err := os.OpenFile(fmt.Sprintf("/proc/%d/fdinfo", pid), os.O_RDONLY, 0)
if err != nil {
t.Fatal(err)
}
defer f.Close()
names, err := f.Readdirnames(0)
if err != nil {
t.Fatal(err)
}
return len(names)
}
before := openFds()
for i := 0; i < 10; i++ {
laddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort("127.0.0.1", "0"))
d := TCPDialer{
Dialer: net.Dialer{
LocalAddr: laddr,
Timeout: 1 * time.Second,
},
}
if _, err := d.DialTCP("127.0.0.1", 1); err == nil {
t.Fatalf("should not succeed")
}
}
if after := openFds(); before != after {
t.Fatalf("could be fd leak, %d %d", before, after)
}
}