Skip to content

Commit f659183

Browse files
committed
os/exec: avoid NewFile on unknown FDs
exec_test.go's init function uses os.NewFile(fd) + f.Stat as a portable mechanism to determine if an FD is in use. Unfortunately, the current use is racy: if an unused FD becomes used between NewFile and f.Close, then we will unintentionally close an FD we do not use. We cannot simply drop Close, as the finalizer will close the FD. We could hold all of the os.Files in a global for the lifetime of the process, but the need for such a hack is indicative of the larger problem: we should not create an os.File for an FD that we do not own. Instead, the new fdtest.Exists provides a helper that performs the equivalent of fstat(2) on each OS to determine if the FD is valid, without using os.File. We also reuse this helper on a variety of other tests that look at open FDs. Fixes #49533 Change-Id: I36e2bdb15f271ab01e55c18db6564271995a15af Reviewed-on: https://go-review.googlesource.com/c/go/+/364035 Trust: Michael Pratt <mpratt@google.com> Run-TryBot: Michael Pratt <mpratt@google.com> Reviewed-by: Bryan C. Mills <bcmills@google.com>
1 parent 6c36c33 commit f659183

File tree

8 files changed

+154
-163
lines changed

8 files changed

+154
-163
lines changed

Diff for: src/go/build/deps_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,9 @@ var depsRules = `
545545
NET, testing, math/rand
546546
< golang.org/x/net/nettest;
547547
548+
syscall
549+
< os/exec/internal/fdtest;
550+
548551
FMT, container/heap, math/rand
549552
< internal/trace;
550553
`

Diff for: src/os/exec/exec_test.go

+20-112
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,20 @@ import (
2121
"net/http/httptest"
2222
"os"
2323
"os/exec"
24+
"os/exec/internal/fdtest"
2425
"path/filepath"
26+
"reflect"
2527
"runtime"
2628
"strconv"
2729
"strings"
2830
"testing"
2931
"time"
3032
)
3133

32-
// haveUnexpectedFDs is set at init time to report whether any
33-
// file descriptors were open at program start.
34+
// haveUnexpectedFDs is set at init time to report whether any file descriptors
35+
// were open at program start.
3436
var haveUnexpectedFDs bool
3537

36-
// unfinalizedFiles holds files that should not be finalized,
37-
// because that would close the associated file descriptor,
38-
// which we don't want to do.
39-
var unfinalizedFiles []*os.File
40-
4138
func init() {
4239
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
4340
return
@@ -49,21 +46,10 @@ func init() {
4946
if poll.IsPollDescriptor(fd) {
5047
continue
5148
}
52-
// We have no good portable way to check whether an FD is open.
53-
// We use NewFile to create a *os.File, which lets us
54-
// know whether it is open, but then we have to cope with
55-
// the finalizer on the *os.File.
56-
f := os.NewFile(fd, "")
57-
if _, err := f.Stat(); err != nil {
58-
// Close the file to clear the finalizer.
59-
// We expect the Close to fail.
60-
f.Close()
61-
} else {
62-
fmt.Printf("fd %d open at test start\n", fd)
49+
50+
if fdtest.Exists(fd) {
6351
haveUnexpectedFDs = true
64-
// Use a global variable to avoid running
65-
// the finalizer, which would close the FD.
66-
unfinalizedFiles = append(unfinalizedFiles, f)
52+
return
6753
}
6854
}
6955
}
@@ -377,50 +363,21 @@ func TestStdinCloseRace(t *testing.T) {
377363

378364
// Issue 5071
379365
func TestPipeLookPathLeak(t *testing.T) {
380-
// If we are reading from /proc/self/fd we (should) get an exact result.
381-
tolerance := 0
382-
383-
// Reading /proc/self/fd is more reliable than calling lsof, so try that
384-
// first.
385-
numOpenFDs := func() (int, []byte, error) {
386-
fds, err := os.ReadDir("/proc/self/fd")
387-
if err != nil {
388-
return 0, nil, err
389-
}
390-
return len(fds), nil, nil
366+
if runtime.GOOS == "windows" {
367+
t.Skip("we don't currently suppore counting open handles on windows")
391368
}
392-
want, before, err := numOpenFDs()
393-
if err != nil {
394-
// We encountered a problem reading /proc/self/fd (we might be on
395-
// a platform that doesn't have it). Fall back onto lsof.
396-
t.Logf("using lsof because: %v", err)
397-
numOpenFDs = func() (int, []byte, error) {
398-
// Android's stock lsof does not obey the -p option,
399-
// so extra filtering is needed.
400-
// https://golang.org/issue/10206
401-
if runtime.GOOS == "android" {
402-
// numOpenFDsAndroid handles errors itself and
403-
// might skip or fail the test.
404-
n, lsof := numOpenFDsAndroid(t)
405-
return n, lsof, nil
406-
}
407-
lsof, err := exec.Command("lsof", "-b", "-n", "-p", strconv.Itoa(os.Getpid())).Output()
408-
return bytes.Count(lsof, []byte("\n")), lsof, err
409-
}
410369

411-
// lsof may see file descriptors associated with the fork itself,
412-
// so we allow some extra margin if we have to use it.
413-
// https://golang.org/issue/19243
414-
tolerance = 5
415-
416-
// Retry reading the number of open file descriptors.
417-
want, before, err = numOpenFDs()
418-
if err != nil {
419-
t.Log(err)
420-
t.Skipf("skipping test; error finding or running lsof")
370+
openFDs := func() []uintptr {
371+
var fds []uintptr
372+
for i := uintptr(0); i < 100; i++ {
373+
if fdtest.Exists(i) {
374+
fds = append(fds, i)
375+
}
421376
}
377+
return fds
422378
}
423379

380+
want := openFDs()
424381
for i := 0; i < 6; i++ {
425382
cmd := exec.Command("something-that-does-not-exist-executable")
426383
cmd.StdoutPipe()
@@ -430,59 +387,10 @@ func TestPipeLookPathLeak(t *testing.T) {
430387
t.Fatal("unexpected success")
431388
}
432389
}
433-
got, after, err := numOpenFDs()
434-
if err != nil {
435-
// numOpenFDs has already succeeded once, it should work here.
436-
t.Errorf("unexpected failure: %v", err)
437-
}
438-
if got-want > tolerance {
439-
t.Errorf("number of open file descriptors changed: got %v, want %v", got, want)
440-
if before != nil {
441-
t.Errorf("before:\n%v\n", before)
442-
}
443-
if after != nil {
444-
t.Errorf("after:\n%v\n", after)
445-
}
446-
}
447-
}
448-
449-
func numOpenFDsAndroid(t *testing.T) (n int, lsof []byte) {
450-
raw, err := exec.Command("lsof").Output()
451-
if err != nil {
452-
t.Skip("skipping test; error finding or running lsof")
453-
}
454-
455-
// First find the PID column index by parsing the first line, and
456-
// select lines containing pid in the column.
457-
pid := []byte(strconv.Itoa(os.Getpid()))
458-
pidCol := -1
459-
460-
s := bufio.NewScanner(bytes.NewReader(raw))
461-
for s.Scan() {
462-
line := s.Bytes()
463-
fields := bytes.Fields(line)
464-
if pidCol < 0 {
465-
for i, v := range fields {
466-
if bytes.Equal(v, []byte("PID")) {
467-
pidCol = i
468-
break
469-
}
470-
}
471-
lsof = append(lsof, line...)
472-
continue
473-
}
474-
if bytes.Equal(fields[pidCol], pid) {
475-
lsof = append(lsof, '\n')
476-
lsof = append(lsof, line...)
477-
}
478-
}
479-
if pidCol < 0 {
480-
t.Fatal("error processing lsof output: unexpected header format")
481-
}
482-
if err := s.Err(); err != nil {
483-
t.Fatalf("error processing lsof output: %v", err)
390+
got := openFDs()
391+
if !reflect.DeepEqual(got, want) {
392+
t.Errorf("set of open file descriptors changed: got %v, want %v", got, want)
484393
}
485-
return bytes.Count(lsof, []byte("\n")), lsof
486394
}
487395

488396
func TestExtraFilesFDShuffle(t *testing.T) {

Diff for: src/os/exec/internal/fdtest/exists_js.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build js
6+
7+
package fdtest
8+
9+
import (
10+
"syscall"
11+
)
12+
13+
// Exists returns true if fd is a valid file descriptor.
14+
func Exists(fd uintptr) bool {
15+
var s syscall.Stat_t
16+
err := syscall.Fstat(int(fd), &s)
17+
return err != syscall.EBADF
18+
}

Diff for: src/os/exec/internal/fdtest/exists_plan9.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build plan9
6+
7+
package fdtest
8+
9+
import (
10+
"syscall"
11+
)
12+
13+
const errBadFd = syscall.ErrorString("fd out of range or not open")
14+
15+
// Exists returns true if fd is a valid file descriptor.
16+
func Exists(fd uintptr) bool {
17+
var buf [1]byte
18+
_, err := syscall.Fstat(int(fd), buf[:])
19+
return err != errBadFd
20+
}

Diff for: src/os/exec/internal/fdtest/exists_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package fdtest
6+
7+
import (
8+
"os"
9+
"runtime"
10+
"testing"
11+
)
12+
13+
func TestExists(t *testing.T) {
14+
if runtime.GOOS == "windows" {
15+
t.Skip("Exists not implemented for windows")
16+
}
17+
18+
if !Exists(os.Stdout.Fd()) {
19+
t.Errorf("Exists(%d) got false want true", os.Stdout.Fd())
20+
}
21+
}

Diff for: src/os/exec/internal/fdtest/exists_unix.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
6+
7+
// Package fdtest provides test helpers for working with file descriptors across exec.
8+
package fdtest
9+
10+
import (
11+
"syscall"
12+
)
13+
14+
// Exists returns true if fd is a valid file descriptor.
15+
func Exists(fd uintptr) bool {
16+
var s syscall.Stat_t
17+
err := syscall.Fstat(int(fd), &s)
18+
return err != syscall.EBADF
19+
}

Diff for: src/os/exec/internal/fdtest/exists_windows.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build windows
6+
7+
package fdtest
8+
9+
// Exists is not implemented on windows and panics.
10+
func Exists(fd uintptr) bool {
11+
panic("unimplemented")
12+
}

0 commit comments

Comments
 (0)