runtime, testing/synctest: breaking bubble isolation with Cond is fatal

sync.Cond.Wait is durably blocking. Waking a goroutine out of Cond.Wait
from outside its bubble panics.

Make this panic a fatal panic, since it leaves the notifyList in an
inconsistent state. We could do some work to make this a recoverable
panic, but the complexity doesn't seem worth the outcome.

For #67434

Change-Id: I88874c1519c2e5c0063175297a9b120cedabcd07
Reviewed-on: https://go-review.googlesource.com/c/go/+/675617
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
Damien Neil 2025-05-22 11:14:53 -07:00 committed by Gopher Robot
parent 555d425d17
commit 21b7e60c6b
4 changed files with 81 additions and 3 deletions

View File

@ -1228,3 +1228,20 @@ func TestFinalizerOrCleanupDeadlock(t *testing.T) {
}) })
} }
} }
func TestSynctestCondSignalFromNoBubble(t *testing.T) {
for _, test := range []string{
"SynctestCond/signal/no_bubble",
"SynctestCond/broadcast/no_bubble",
"SynctestCond/signal/other_bubble",
"SynctestCond/broadcast/other_bubble",
} {
t.Run(test, func(t *testing.T) {
output := runTestProg(t, "testprog", test)
want := "fatal error: semaphore wake of synctest goroutine from outside bubble"
if !strings.Contains(output, want) {
t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
}
})
}
}

View File

@ -635,7 +635,7 @@ func notifyListNotifyAll(l *notifyList) {
s.next = nil s.next = nil
if s.g.bubble != nil && getg().bubble != s.g.bubble { if s.g.bubble != nil && getg().bubble != s.g.bubble {
println("semaphore wake of synctest goroutine", s.g.goid, "from outside bubble") println("semaphore wake of synctest goroutine", s.g.goid, "from outside bubble")
panic("semaphore wake of synctest goroutine from outside bubble") fatal("semaphore wake of synctest goroutine from outside bubble")
} }
readyWithTime(s, 4) readyWithTime(s, 4)
s = next s = next
@ -692,7 +692,7 @@ func notifyListNotifyOne(l *notifyList) {
s.next = nil s.next = nil
if s.g.bubble != nil && getg().bubble != s.g.bubble { if s.g.bubble != nil && getg().bubble != s.g.bubble {
println("semaphore wake of synctest goroutine", s.g.goid, "from outside bubble") println("semaphore wake of synctest goroutine", s.g.goid, "from outside bubble")
panic("semaphore wake of synctest goroutine from outside bubble") fatal("semaphore wake of synctest goroutine from outside bubble")
} }
readyWithTime(s, 4) readyWithTime(s, 4)
return return

View File

@ -0,0 +1,58 @@
// Copyright 2025 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 main
import (
"internal/synctest"
"sync"
)
func init() {
register("SynctestCond/signal/no_bubble", func() {
synctestCond(func(cond *sync.Cond) {
cond.Signal()
})
})
register("SynctestCond/broadcast/no_bubble", func() {
synctestCond(func(cond *sync.Cond) {
cond.Broadcast()
})
})
register("SynctestCond/signal/other_bubble", func() {
synctestCond(func(cond *sync.Cond) {
synctest.Run(cond.Signal)
})
})
register("SynctestCond/broadcast/other_bubble", func() {
synctestCond(func(cond *sync.Cond) {
synctest.Run(cond.Broadcast)
})
})
}
func synctestCond(f func(*sync.Cond)) {
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
readyc = make(chan struct{})
wg sync.WaitGroup
)
defer wg.Wait()
wg.Go(func() {
synctest.Run(func() {
go func() {
mu.Lock()
defer mu.Unlock()
cond.Wait()
}()
synctest.Wait()
<-readyc // #1: signal that cond.Wait is waiting
<-readyc // #2: wait to continue
cond.Signal()
})
})
readyc <- struct{}{}
f(cond)
}

View File

@ -92,7 +92,10 @@
// //
// A [sync.WaitGroup] becomes associated with a bubble on the first // A [sync.WaitGroup] becomes associated with a bubble on the first
// call to Add or Go. Once a WaitGroup is associated with a bubble, // call to Add or Go. Once a WaitGroup is associated with a bubble,
// calling Add or Go from outside that bubble panics. // calling Add or Go from outside that bubble is a fatal error.
//
// [sync.Cond.Wait] is durably blocking. Waking a goroutine in a bubble
// blocked on Cond.Wait from outside the bubble is a fatal error.
// //
// Cleanup functions and finalizers registered with // Cleanup functions and finalizers registered with
// [runtime.AddCleanup] and [runtime.SetFinalizer] // [runtime.AddCleanup] and [runtime.SetFinalizer]