Skip to content

Commit 8d6fc84

Browse files
internal/poll: don't take read lock in SetBlocking
Taking a read lock in SetBlocking could cause SetBlocking to block waiting for a Read in another goroutine to complete. Since SetBlocking is called by os.(*File).Fd, that could lead to deadlock if the goroutine calling Fd is going to use it to unblock the Read. Use an atomic store instead. Updates #24481 Change-Id: I79413328e06ddf28b6d5b8af7a0e29d5b4e1e6ff Reviewed-on: https://go-review.googlesource.com/123176 Run-TryBot: Ian Lance Taylor <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent e86bbc2 commit 8d6fc84

File tree

3 files changed

+56
-13
lines changed

3 files changed

+56
-13
lines changed

src/cmd/dist/test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1354,7 +1354,7 @@ func (t *tester) runFlag(rx string) string {
13541354
func (t *tester) raceTest(dt *distTest) error {
13551355
t.addCmd(dt, "src", t.goTest(), "-race", "-i", "runtime/race", "flag", "os", "os/exec")
13561356
t.addCmd(dt, "src", t.goTest(), "-race", t.runFlag("Output"), "runtime/race")
1357-
t.addCmd(dt, "src", t.goTest(), "-race", t.runFlag("TestParse|TestEcho|TestStdinCloseRace|TestClosedPipeRace|TestTypeRace|TestFdRace|TestFileCloseRace"), "flag", "net", "os", "os/exec", "encoding/gob")
1357+
t.addCmd(dt, "src", t.goTest(), "-race", t.runFlag("TestParse|TestEcho|TestStdinCloseRace|TestClosedPipeRace|TestTypeRace|TestFdRace|TestFdReadRace|TestFileCloseRace"), "flag", "net", "os", "os/exec", "encoding/gob")
13581358
// We don't want the following line, because it
13591359
// slows down all.bash (by 10 seconds on my laptop).
13601360
// The race builder should catch any error here, but doesn't.

src/internal/poll/fd_unix.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type FD struct {
3131
// Semaphore signaled when file is closed.
3232
csema uint32
3333

34+
// Non-zero if this file has been set to blocking mode.
35+
isBlocking uint32
36+
3437
// Whether this is a streaming descriptor, as opposed to a
3538
// packet-based descriptor like a UDP socket. Immutable.
3639
IsStream bool
@@ -41,9 +44,6 @@ type FD struct {
4144

4245
// Whether this is a file rather than a network socket.
4346
isFile bool
44-
45-
// Whether this file has been set to blocking mode.
46-
isBlocking bool
4747
}
4848

4949
// Init initializes the FD. The Sysfd field should already be set.
@@ -57,14 +57,14 @@ func (fd *FD) Init(net string, pollable bool) error {
5757
fd.isFile = true
5858
}
5959
if !pollable {
60-
fd.isBlocking = true
60+
fd.isBlocking = 1
6161
return nil
6262
}
6363
err := fd.pd.init(fd)
6464
if err != nil {
6565
// If we could not initialize the runtime poller,
6666
// assume we are using blocking mode.
67-
fd.isBlocking = true
67+
fd.isBlocking = 1
6868
}
6969
return err
7070
}
@@ -103,9 +103,9 @@ func (fd *FD) Close() error {
103103
// reference, it is already closed. Only wait if the file has
104104
// not been set to blocking mode, as otherwise any current I/O
105105
// may be blocking, and that would block the Close.
106-
// No need for a lock to read isBlocking, increfAndClose means
106+
// No need for an atomic read of isBlocking, increfAndClose means
107107
// we have exclusive access to fd.
108-
if !fd.isBlocking {
108+
if fd.isBlocking == 0 {
109109
runtime_Semacquire(&fd.csema)
110110
}
111111

@@ -123,13 +123,14 @@ func (fd *FD) Shutdown(how int) error {
123123

124124
// SetBlocking puts the file into blocking mode.
125125
func (fd *FD) SetBlocking() error {
126-
// Take an exclusive lock, rather than calling incref, so that
127-
// we can safely modify isBlocking.
128-
if err := fd.readLock(); err != nil {
126+
if err := fd.incref(); err != nil {
129127
return err
130128
}
131-
defer fd.readUnlock()
132-
fd.isBlocking = true
129+
defer fd.decref()
130+
// Atomic store so that concurrent calls to SetBlocking
131+
// do not cause a race condition. isBlocking only ever goes
132+
// from 0 to 1 so there is no real race here.
133+
atomic.StoreUint32(&fd.isBlocking, 1)
133134
return syscall.SetNonblock(fd.Sysfd, false)
134135
}
135136

src/os/pipe_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,45 @@ func TestFdRace(t *testing.T) {
395395
}
396396
wg.Wait()
397397
}
398+
399+
func TestFdReadRace(t *testing.T) {
400+
t.Parallel()
401+
402+
r, w, err := os.Pipe()
403+
if err != nil {
404+
t.Fatal(err)
405+
}
406+
defer r.Close()
407+
defer w.Close()
408+
409+
c := make(chan bool)
410+
var wg sync.WaitGroup
411+
wg.Add(1)
412+
go func() {
413+
defer wg.Done()
414+
var buf [10]byte
415+
r.SetReadDeadline(time.Now().Add(time.Second))
416+
c <- true
417+
if _, err := r.Read(buf[:]); os.IsTimeout(err) {
418+
t.Error("read timed out")
419+
}
420+
}()
421+
422+
wg.Add(1)
423+
go func() {
424+
defer wg.Done()
425+
<-c
426+
// Give the other goroutine a chance to enter the Read.
427+
// It doesn't matter if this occasionally fails, the test
428+
// will still pass, it just won't test anything.
429+
time.Sleep(10 * time.Millisecond)
430+
r.Fd()
431+
432+
// The bug was that Fd would hang until Read timed out.
433+
// If the bug is fixed, then closing r here will cause
434+
// the Read to exit before the timeout expires.
435+
r.Close()
436+
}()
437+
438+
wg.Wait()
439+
}

0 commit comments

Comments
 (0)