Go version

go version go1.25rc1 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/alexhamlin/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/alexhamlin/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/1v/jwmc7lqn4sv37y_yppfyr32c0000gn/T/go-build478068048=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/alexhamlin/Desktop/synctest-waitgroup-go/go.mod'
GOMODCACHE='/Users/alexhamlin/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/alexhamlin/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/alexhamlin/sdk/go1.25rc1'
GOSUMDB='sum.golang.org'
GOTELEMETRY='on'
GOTELEMETRYDIR='/Users/alexhamlin/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/alexhamlin/sdk/go1.25rc1/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25rc1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

https://go.dev/play/p/4itIC1JGITi?v=gotip

Alternatively, go test -count=10000 with the case below that runs fewer iterations in the inner loop. go test -race -count=1000 also reliably reproduces on my M1 MacBook Pro.

More observations:

  • Further synchronization in the goroutine (e.g. incrementing a counter under a mutex) also makes reproduction more reliable.
  • go version go1.25-devel_b5d555991a on darwin/arm64 reproduces as well (when I make the go.mod + GOTOOLCHAIN updates to ensure it's really used).
  • linux/arm64 (Pine64 ROCKPro64) and linux/amd64 (random VPS host) reproduce as well.
  • It almost goes without saying, but sync.WaitGroup.Go reproduces this just as well as the classic Add + Done variant shown below (in fact, my attempt to use the new API is how I noticed this).
  • I'm not able to reproduce this with a single wg.Add(100) before entering the loop.
module github.com/ahamlinman/synctest-waitgroup-go

go 1.25rc1
package main

import (
    "sync"
    "testing"
    "testing/synctest"
)

func TestSynctestWaitGroupGo(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var wg sync.WaitGroup
        for range 100 {
            wg.Add(1)
            go func() {
                defer wg.Done()
            }()
        }
        wg.Wait()
    })
}

What did you see happen?

One of the following two kinds of crashes.

panic: sync: negative WaitGroup counter

panic: sync: negative WaitGroup counter

goroutine 808574 [running, synctest bubble 7850]:
sync.(*WaitGroup).Add(0x14000380cc0, 0xffffffffffffffff)
        /Users/alexhamlin/sdk/go1.25rc1/src/sync/waitgroup.go:118 +0x288
sync.(*WaitGroup).Done(...)
        /Users/alexhamlin/sdk/go1.25rc1/src/sync/waitgroup.go:158
github.com/ahamlinman/synctest-waitgroup-go.TestSynctestWaitGroupGo.func1.1()
        /Users/alexhamlin/Desktop/synctest-waitgroup-go/main_test.go:16 +0x48
created by github.com/ahamlinman/synctest-waitgroup-go.TestSynctestWaitGroupGo.func1 in goroutine 805551
        /Users/alexhamlin/Desktop/synctest-waitgroup-go/main_test.go:14 +0x38
exit status 2
FAIL    github.com/ahamlinman/synctest-waitgroup-go     0.530s

fatal error: sync: WaitGroup.Add called from inside and outside synctest bubble

fatal error: sync: WaitGroup.Add called from inside and outside synctest bubble

goroutine 236056 [running, synctest bubble 2886]:
sync.fatal({0x1021e8fe3?, 0x1021d20a8?})
        /Users/alexhamlin/sdk/go1.25rc1/src/runtime/panic.go:1026 +0x20
sync.(*WaitGroup).Add(0x14000182050, 0x1)
        /Users/alexhamlin/sdk/go1.25rc1/src/sync/waitgroup.go:100 +0xe8
github.com/ahamlinman/synctest-waitgroup-go.TestSynctestWaitGroupGo.func1(0x14000246380?)
        /Users/alexhamlin/Desktop/synctest-waitgroup-go/main_test.go:13 +0x58
testing.tRunner(0x14000246380, 0x10225d370)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:1931 +0xc8
created by testing/synctest.testingSynctestTest in goroutine 236055
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2043 +0x1fc

goroutine 1 [chan receive]:
testing.(*T).Run(0x14000246000, {0x1021e126a?, 0x1400007ab38?}, 0x10225d2e0)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2002 +0x378
testing.runTests.func1(0x14000246000)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2474 +0x38
testing.tRunner(0x14000246000, 0x1400007ac68)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:1931 +0xc8
testing.runTests(0x1400000c030, {0x102348c80, 0x1, 0x1}, {0x140000681e0?, 0x7?, 0x102352a20?})
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2472 +0x3b8
testing.(*M).Run(0x1400007e140)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2334 +0x530
main.main()
        _testmain.go:45 +0x80

goroutine 236055 [chan receive (durable), synctest bubble 2886]:
testing/synctest.testingSynctestTest(0x140002461c0, 0x10225d370)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:2044 +0x210
testing/synctest.Test.func1()
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/synctest/synctest.go:283 +0x28
created by testing/synctest.Test in goroutine 236054
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/synctest/synctest.go:282 +0x88

goroutine 236054 [synctest.Run (durable), synctest bubble 2886]:
internal/synctest.Run(0x14000356020)
        /Users/alexhamlin/sdk/go1.25rc1/src/runtime/synctest.go:218 +0x1b4
testing/synctest.Test(0x140002461c0, 0x10225d370)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/synctest/synctest.go:282 +0x88
github.com/ahamlinman/synctest-waitgroup-go.TestSynctestWaitGroupGo(0x140002461c0?)
        /Users/alexhamlin/Desktop/synctest-waitgroup-go/main_test.go:10 +0x24
testing.tRunner(0x140002461c0, 0x10225d2e0)
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:1931 +0xc8
created by testing.(*T).Run in goroutine 1
        /Users/alexhamlin/sdk/go1.25rc1/src/testing/testing.go:1994 +0x364
exit status 2
FAIL    github.com/ahamlinman/synctest-waitgroup-go     0.328s

What did you expect to see?

A passing test, given that the entire sync.WaitGroup and all operations on it are contained in a single synctest bubble.

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)