From 9f89a857f7622703b7dd5f418435caf6cd627493 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Wed, 5 Nov 2025 00:55:49 +0800 Subject: [PATCH] sync: repanic when f() panics for WaitGroup.Go This is a copy-paste of https://github.com/golang/go/issues/76126#issuecomment-3473417226 by adonovan. Fixes #76126 Fixes #74702 --- src/sync/waitgroup.go | 13 ++++++++++++- src/sync/waitgroup_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/sync/waitgroup.go b/src/sync/waitgroup.go index 195f839da417bd..dcc5636c4bd80c 100644 --- a/src/sync/waitgroup.go +++ b/src/sync/waitgroup.go @@ -236,7 +236,18 @@ func (wg *WaitGroup) Wait() { func (wg *WaitGroup) Go(f func()) { wg.Add(1) go func() { - defer wg.Done() + defer func() { + if x := recover(); x != nil { + // Don't call Done as it may cause Wait to unblock, + // so that the main goroutine races with the runtime.fatal + // resulting from unhandled panic. + panic(x) + } + + // f completed normally, or abruptly using goexit. + // Either way, decrement the semaphore. + wg.Done() + }() f() }() } diff --git a/src/sync/waitgroup_test.go b/src/sync/waitgroup_test.go index 8a948f8972c8a7..929e0e9055b984 100644 --- a/src/sync/waitgroup_test.go +++ b/src/sync/waitgroup_test.go @@ -5,6 +5,12 @@ package sync_test import ( + "bytes" + "internal/testenv" + "os" + "os/exec" + "strings" + "sync" . "sync" "sync/atomic" "testing" @@ -110,6 +116,38 @@ func TestWaitGroupGo(t *testing.T) { } } +// This test ensures that an unhandled panic in a Go goroutine terminates +// the process without causing Wait to unblock; previously there was a race. +func TestIssue76126(t *testing.T) { + testenv.MustHaveExec(t) + // Call child in a child process + // and inspect its failure message. + cmd := exec.Command(os.Args[0], "-test.run=^TestIssue76126Child$") + cmd.Env = append(os.Environ(), "SYNC_TEST_CHILD=1") + buf := new(bytes.Buffer) + cmd.Stderr = buf + cmd.Run() // ignore error + + got := buf.String() + if strings.Contains(got, "panic: test") { + // ok + } else { + t.Errorf("missing panic: test\n%s", got) + } +} + +func TestIssue76126Child(t *testing.T) { + if os.Getenv("SYNC_TEST_CHILD") != "1" { + t.Skip("not child") + } + var wg sync.WaitGroup + wg.Go(func() { + panic("test") + }) + wg.Wait() // process should terminate here + panic("Wait returned") // must not be reached +} + func BenchmarkWaitGroupUncontended(b *testing.B) { type PaddedWaitGroup struct { WaitGroup