diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 2f68cbcffc77c65bf32e02113a237a2eb5e1c8b5..7f25038d2d09da29f507b8c750611033e0cc05ab 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -545,6 +545,9 @@ var depsRules = ` NET, testing, math/rand < golang.org/x/net/nettest; + syscall + < os/exec/internal/fdtest; + FMT, container/heap, math/rand < internal/trace; ` diff --git a/src/os/exec/exec_test.go b/src/os/exec/exec_test.go index 459ba39dff8f50a48b00d496289555f4a0e35ad6..6172c78dd4d90c8295094ca3375d6360d80a68c6 100644 --- a/src/os/exec/exec_test.go +++ b/src/os/exec/exec_test.go @@ -21,7 +21,9 @@ import ( "net/http/httptest" "os" "os/exec" + "os/exec/internal/fdtest" "path/filepath" + "reflect" "runtime" "strconv" "strings" @@ -29,15 +31,10 @@ import ( "time" ) -// haveUnexpectedFDs is set at init time to report whether any -// file descriptors were open at program start. +// haveUnexpectedFDs is set at init time to report whether any file descriptors +// were open at program start. var haveUnexpectedFDs bool -// unfinalizedFiles holds files that should not be finalized, -// because that would close the associated file descriptor, -// which we don't want to do. -var unfinalizedFiles []*os.File - func init() { if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { return @@ -49,21 +46,10 @@ func init() { if poll.IsPollDescriptor(fd) { continue } - // We have no good portable way to check whether an FD is open. - // We use NewFile to create a *os.File, which lets us - // know whether it is open, but then we have to cope with - // the finalizer on the *os.File. - f := os.NewFile(fd, "") - if _, err := f.Stat(); err != nil { - // Close the file to clear the finalizer. - // We expect the Close to fail. - f.Close() - } else { - fmt.Printf("fd %d open at test start\n", fd) + + if fdtest.Exists(fd) { haveUnexpectedFDs = true - // Use a global variable to avoid running - // the finalizer, which would close the FD. - unfinalizedFiles = append(unfinalizedFiles, f) + return } } } @@ -377,50 +363,21 @@ func TestStdinCloseRace(t *testing.T) { // Issue 5071 func TestPipeLookPathLeak(t *testing.T) { - // If we are reading from /proc/self/fd we (should) get an exact result. - tolerance := 0 - - // Reading /proc/self/fd is more reliable than calling lsof, so try that - // first. - numOpenFDs := func() (int, []byte, error) { - fds, err := os.ReadDir("/proc/self/fd") - if err != nil { - return 0, nil, err - } - return len(fds), nil, nil + if runtime.GOOS == "windows" { + t.Skip("we don't currently suppore counting open handles on windows") } - want, before, err := numOpenFDs() - if err != nil { - // We encountered a problem reading /proc/self/fd (we might be on - // a platform that doesn't have it). Fall back onto lsof. - t.Logf("using lsof because: %v", err) - numOpenFDs = func() (int, []byte, error) { - // Android's stock lsof does not obey the -p option, - // so extra filtering is needed. - // https://golang.org/issue/10206 - if runtime.GOOS == "android" { - // numOpenFDsAndroid handles errors itself and - // might skip or fail the test. - n, lsof := numOpenFDsAndroid(t) - return n, lsof, nil - } - lsof, err := exec.Command("lsof", "-b", "-n", "-p", strconv.Itoa(os.Getpid())).Output() - return bytes.Count(lsof, []byte("\n")), lsof, err - } - // lsof may see file descriptors associated with the fork itself, - // so we allow some extra margin if we have to use it. - // https://golang.org/issue/19243 - tolerance = 5 - - // Retry reading the number of open file descriptors. - want, before, err = numOpenFDs() - if err != nil { - t.Log(err) - t.Skipf("skipping test; error finding or running lsof") + openFDs := func() []uintptr { + var fds []uintptr + for i := uintptr(0); i < 100; i++ { + if fdtest.Exists(i) { + fds = append(fds, i) + } } + return fds } + want := openFDs() for i := 0; i < 6; i++ { cmd := exec.Command("something-that-does-not-exist-executable") cmd.StdoutPipe() @@ -430,59 +387,10 @@ func TestPipeLookPathLeak(t *testing.T) { t.Fatal("unexpected success") } } - got, after, err := numOpenFDs() - if err != nil { - // numOpenFDs has already succeeded once, it should work here. - t.Errorf("unexpected failure: %v", err) - } - if got-want > tolerance { - t.Errorf("number of open file descriptors changed: got %v, want %v", got, want) - if before != nil { - t.Errorf("before:\n%v\n", before) - } - if after != nil { - t.Errorf("after:\n%v\n", after) - } - } -} - -func numOpenFDsAndroid(t *testing.T) (n int, lsof []byte) { - raw, err := exec.Command("lsof").Output() - if err != nil { - t.Skip("skipping test; error finding or running lsof") - } - - // First find the PID column index by parsing the first line, and - // select lines containing pid in the column. - pid := []byte(strconv.Itoa(os.Getpid())) - pidCol := -1 - - s := bufio.NewScanner(bytes.NewReader(raw)) - for s.Scan() { - line := s.Bytes() - fields := bytes.Fields(line) - if pidCol < 0 { - for i, v := range fields { - if bytes.Equal(v, []byte("PID")) { - pidCol = i - break - } - } - lsof = append(lsof, line...) - continue - } - if bytes.Equal(fields[pidCol], pid) { - lsof = append(lsof, '\n') - lsof = append(lsof, line...) - } - } - if pidCol < 0 { - t.Fatal("error processing lsof output: unexpected header format") - } - if err := s.Err(); err != nil { - t.Fatalf("error processing lsof output: %v", err) + got := openFDs() + if !reflect.DeepEqual(got, want) { + t.Errorf("set of open file descriptors changed: got %v, want %v", got, want) } - return bytes.Count(lsof, []byte("\n")), lsof } func TestExtraFilesFDShuffle(t *testing.T) { diff --git a/src/os/exec/internal/fdtest/exists_js.go b/src/os/exec/internal/fdtest/exists_js.go new file mode 100644 index 0000000000000000000000000000000000000000..a7ce33c74f4259a4e5cfce7b2d7158fe5931c974 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_js.go @@ -0,0 +1,18 @@ +// Copyright 2021 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 js + +package fdtest + +import ( + "syscall" +) + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var s syscall.Stat_t + err := syscall.Fstat(int(fd), &s) + return err != syscall.EBADF +} diff --git a/src/os/exec/internal/fdtest/exists_plan9.go b/src/os/exec/internal/fdtest/exists_plan9.go new file mode 100644 index 0000000000000000000000000000000000000000..8886e0602715ebcf3a5d59d854ec71f6cf0e0edc --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_plan9.go @@ -0,0 +1,20 @@ +// Copyright 2021 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 plan9 + +package fdtest + +import ( + "syscall" +) + +const errBadFd = syscall.ErrorString("fd out of range or not open") + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var buf [1]byte + _, err := syscall.Fstat(int(fd), buf[:]) + return err != errBadFd +} diff --git a/src/os/exec/internal/fdtest/exists_test.go b/src/os/exec/internal/fdtest/exists_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a02dddf7f7de5e0e7f989770477365aab99460a0 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_test.go @@ -0,0 +1,21 @@ +// Copyright 2021 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. + +package fdtest + +import ( + "os" + "runtime" + "testing" +) + +func TestExists(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Exists not implemented for windows") + } + + if !Exists(os.Stdout.Fd()) { + t.Errorf("Exists(%d) got false want true", os.Stdout.Fd()) + } +} diff --git a/src/os/exec/internal/fdtest/exists_unix.go b/src/os/exec/internal/fdtest/exists_unix.go new file mode 100644 index 0000000000000000000000000000000000000000..49f264cebdaad0a96f31349090d243d39c83b6c2 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_unix.go @@ -0,0 +1,19 @@ +// Copyright 2021 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +// Package fdtest provides test helpers for working with file descriptors across exec. +package fdtest + +import ( + "syscall" +) + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var s syscall.Stat_t + err := syscall.Fstat(int(fd), &s) + return err != syscall.EBADF +} diff --git a/src/os/exec/internal/fdtest/exists_windows.go b/src/os/exec/internal/fdtest/exists_windows.go new file mode 100644 index 0000000000000000000000000000000000000000..72b8ccfd23c47e8e04932822f72fbf4a5c4ff449 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_windows.go @@ -0,0 +1,12 @@ +// Copyright 2021 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 windows + +package fdtest + +// Exists is not implemented on windows and panics. +func Exists(fd uintptr) bool { + panic("unimplemented") +} diff --git a/src/os/exec/read3.go b/src/os/exec/read3.go index 8aae5735c49caff3ca42c9c4a9b5a6ea94d11e97..10cbfbd54abfee9950f6360d658ccdcd1d192b42 100644 --- a/src/os/exec/read3.go +++ b/src/os/exec/read3.go @@ -18,12 +18,15 @@ import ( "io" "os" "os/exec" + "os/exec/internal/fdtest" "runtime" "strings" ) func main() { fd3 := os.NewFile(3, "fd3") + defer fd3.Close() + bs, err := io.ReadAll(fd3) if err != nil { fmt.Printf("ReadAll from fd 3: %v\n", err) @@ -37,65 +40,52 @@ func main() { // descriptor from parent == 3 // All descriptors 4 and up should be available, // except for any used by the network poller. - var files []*os.File - for wantfd := uintptr(4); wantfd <= 100; wantfd++ { - if poll.IsPollDescriptor(wantfd) { + for fd := uintptr(4); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { continue } - f, err := os.Open(os.Args[0]) - if err != nil { - fmt.Printf("error opening file with expected fd %d: %v", wantfd, err) - os.Exit(1) + + if !fdtest.Exists(fd) { + continue } - if got := f.Fd(); got != wantfd { - fmt.Printf("leaked parent file. fd = %d; want %d\n", got, wantfd) - fdfile := fmt.Sprintf("/proc/self/fd/%d", wantfd) - link, err := os.Readlink(fdfile) - fmt.Printf("readlink(%q) = %q, %v\n", fdfile, link, err) - var args []string - switch runtime.GOOS { - case "plan9": - args = []string{fmt.Sprintf("/proc/%d/fd", os.Getpid())} - case "aix", "solaris", "illumos": - args = []string{fmt.Sprint(os.Getpid())} - default: - args = []string{"-p", fmt.Sprint(os.Getpid())} - } - // Determine which command to use to display open files. - ofcmd := "lsof" - switch runtime.GOOS { - case "dragonfly", "freebsd", "netbsd", "openbsd": - ofcmd = "fstat" - case "plan9": - ofcmd = "/bin/cat" - case "aix": - ofcmd = "procfiles" - case "solaris", "illumos": - ofcmd = "pfiles" - } + fmt.Printf("leaked parent file. fdtest.Exists(%d) got true want false\n", fd) + + fdfile := fmt.Sprintf("/proc/self/fd/%d", fd) + link, err := os.Readlink(fdfile) + fmt.Printf("readlink(%q) = %q, %v\n", fdfile, link, err) - cmd := exec.Command(ofcmd, args...) - out, err := cmd.CombinedOutput() - if err != nil { - fmt.Fprintf(os.Stderr, "%s failed: %v\n", strings.Join(cmd.Args, " "), err) - } - fmt.Printf("%s", out) - os.Exit(1) + var args []string + switch runtime.GOOS { + case "plan9": + args = []string{fmt.Sprintf("/proc/%d/fd", os.Getpid())} + case "aix", "solaris", "illumos": + args = []string{fmt.Sprint(os.Getpid())} + default: + args = []string{"-p", fmt.Sprint(os.Getpid())} } - files = append(files, f) - } - for _, f := range files { - f.Close() - } + // Determine which command to use to display open files. + ofcmd := "lsof" + switch runtime.GOOS { + case "dragonfly", "freebsd", "netbsd", "openbsd": + ofcmd = "fstat" + case "plan9": + ofcmd = "/bin/cat" + case "aix": + ofcmd = "procfiles" + case "solaris", "illumos": + ofcmd = "pfiles" + } - // Referring to fd3 here ensures that it is not - // garbage collected, and therefore closed, while - // executing the wantfd loop above. It doesn't matter - // what we do with fd3 as long as we refer to it; - // closing it is the easy choice. - fd3.Close() + cmd := exec.Command(ofcmd, args...) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", strings.Join(cmd.Args, " "), err) + } + fmt.Printf("%s", out) + os.Exit(1) + } os.Stdout.Write(bs) }