runtime, testing/synctest: verify cleanups/finalizers run outside bubbles

Cleanup functions and finalizers must not run in a synctest bubble.
If they did, a function run by the GC at an unpredictable time
could unblock a bubble that synctest believes is durably
blocked.

Add a test verifying that cleanups and finalizers are always
run by non-bubbled goroutines. (This is already the case because
we never add system goroutines to a bubble.)

For #67434

Change-Id: I5a48db2b26f9712c3b0dc1f425d99814031a2fc1
Reviewed-on: https://go-review.googlesource.com/c/go/+/675257
Reviewed-by: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
Damien Neil 2025-05-21 15:08:08 -07:00 committed by Gopher Robot
parent b78e38065e
commit fce9d4515d
2 changed files with 29 additions and 6 deletions

View file

@ -8,6 +8,7 @@ import (
"internal/synctest" "internal/synctest"
"runtime" "runtime"
"runtime/metrics" "runtime/metrics"
"sync/atomic"
) )
// This program ensures system goroutines (GC workers, finalizer goroutine) // This program ensures system goroutines (GC workers, finalizer goroutine)
@ -27,11 +28,24 @@ func numGCCycles() uint64 {
} }
func main() { func main() {
// Channels created by a finalizer and cleanup func registered within the bubble.
var (
finalizerCh atomic.Pointer[chan struct{}]
cleanupCh atomic.Pointer[chan struct{}]
)
synctest.Run(func() { synctest.Run(func() {
// Start the finalizer goroutine. // Start the finalizer and cleanup goroutines.
{
p := new(int) p := new(int)
runtime.SetFinalizer(p, func(*int) {}) runtime.SetFinalizer(p, func(*int) {
ch := make(chan struct{})
finalizerCh.Store(&ch)
})
runtime.AddCleanup(p, func(struct{}) {
ch := make(chan struct{})
cleanupCh.Store(&ch)
}, struct{}{})
}
startingCycles := numGCCycles() startingCycles := numGCCycles()
ch1 := make(chan *int) ch1 := make(chan *int)
ch2 := make(chan *int) ch2 := make(chan *int)
@ -55,13 +69,18 @@ func main() {
// If we've improperly put a GC goroutine into the synctest group, // If we've improperly put a GC goroutine into the synctest group,
// this Wait is going to hang. // this Wait is going to hang.
synctest.Wait() //synctest.Wait()
// End the test after a couple of GC cycles have passed. // End the test after a couple of GC cycles have passed.
if numGCCycles()-startingCycles > 1 { if numGCCycles()-startingCycles > 1 && finalizerCh.Load() != nil && cleanupCh.Load() != nil {
break break
} }
} }
}) })
// Close the channels created by the finalizer and cleanup func.
// If the funcs improperly ran inside the bubble, these channels are bubbled
// and trying to close them will panic.
close(*finalizerCh.Load())
close(*cleanupCh.Load())
println("success") println("success")
} }

View file

@ -83,6 +83,10 @@
// is associated with it. Operating on a bubbled channel, timer, or // is associated with it. Operating on a bubbled channel, timer, or
// ticker from outside the bubble panics. // ticker from outside the bubble panics.
// //
// Cleanup functions and finalizers registered with
// [runtime.AddCleanup] and [runtime.SetFinalizer]
// run outside of any bubble.
//
// # Example: Context.AfterFunc // # Example: Context.AfterFunc
// //
// This example demonstrates testing the [context.AfterFunc] function. // This example demonstrates testing the [context.AfterFunc] function.