Skip to content

Commit 95a0e5a

Browse files
callthingsoffgopherbot
authored andcommitted
sync: don't call Done when f() panics in WaitGroup.Go
This change is based on #76126 (comment) by adonovan. Fixes #76126 Fixes #74702 Change-Id: Ie404d8204be8917fa8a7b414bb6d319238267c83 GitHub-Last-Rev: b1beddc GitHub-Pull-Request: #76172 Reviewed-on: https://go-review.googlesource.com/c/go/+/717760 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Mateusz Poliwczak <mpoliwczak34@gmail.com> Auto-Submit: Michael Pratt <mpratt@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> Reviewed-by: Michael Pratt <mpratt@google.com>
1 parent e8ed85d commit 95a0e5a

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

src/sync/waitgroup.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,25 @@ func (wg *WaitGroup) Wait() {
236236
func (wg *WaitGroup) Go(f func()) {
237237
wg.Add(1)
238238
go func() {
239-
defer wg.Done()
239+
defer func() {
240+
if x := recover(); x != nil {
241+
// f panicked, which will be fatal because
242+
// this is a new goroutine.
243+
//
244+
// Calling Done will unblock Wait in the main goroutine,
245+
// allowing it to race with the fatal panic and
246+
// possibly even exit the process (os.Exit(0))
247+
// before the panic completes.
248+
//
249+
// This is almost certainly undesirable,
250+
// so instead avoid calling Done and simply panic.
251+
panic(x)
252+
}
253+
254+
// f completed normally, or abruptly using goexit.
255+
// Either way, decrement the semaphore.
256+
wg.Done()
257+
}()
240258
f()
241259
}()
242260
}

src/sync/waitgroup_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
package sync_test
66

77
import (
8+
"bytes"
9+
"internal/testenv"
10+
"os"
11+
"os/exec"
12+
"strings"
813
. "sync"
914
"sync/atomic"
1015
"testing"
@@ -110,6 +115,32 @@ func TestWaitGroupGo(t *testing.T) {
110115
}
111116
}
112117

118+
// This test ensures that an unhandled panic in a Go goroutine terminates
119+
// the process without causing Wait to unblock; previously there was a race.
120+
func TestIssue76126(t *testing.T) {
121+
testenv.MustHaveExec(t)
122+
if os.Getenv("SYNC_TEST_CHILD") != "1" {
123+
// Call child in a child process
124+
// and inspect its failure message.
125+
cmd := exec.Command(os.Args[0], "-test.run=^TestIssue76126$")
126+
cmd.Env = append(os.Environ(), "SYNC_TEST_CHILD=1")
127+
buf := new(bytes.Buffer)
128+
cmd.Stderr = buf
129+
cmd.Run() // ignore error
130+
got := buf.String()
131+
if !strings.Contains(got, "panic: test") {
132+
t.Errorf("missing panic: test\n%s", got)
133+
}
134+
return
135+
}
136+
var wg WaitGroup
137+
wg.Go(func() {
138+
panic("test")
139+
})
140+
wg.Wait() // process should terminate here
141+
panic("Wait returned") // must not be reached
142+
}
143+
113144
func BenchmarkWaitGroupUncontended(b *testing.B) {
114145
type PaddedWaitGroup struct {
115146
WaitGroup

0 commit comments

Comments
 (0)