Repository navigation
net: lazily created semaphore channels associate with a synctest bubble #80903
Description
Go version
go version go1.26.6 darwin/arm64
Output of go env in your module/workspace
GOVERSION='go1.26.6' GOOS='darwin' GOARCH='arm64' CGO_ENABLED='1'
GOEXPERIMENT='' GODEBUG='' GOFLAGS=''
Does this issue reproduce with the latest release?
Yes, on go1.26.6. Also at master 72aa6db, where the change below was tested.
What did you do?
Two net.LookupIP calls, the first inside a synctest bubble and the second outside it, with no dependencies:
package main
import (
"net"
"testing"
"testing/synctest"
)
func TestBubbleFirstThenOutside(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
net.LookupIP("localhost")
})
net.LookupIP("localhost")
}The same two calls in the reverse order pass, which is what makes this an ordering problem rather than a bubble-versus-network one:
func TestOutsideFirstThenBubble(t *testing.T) {
net.LookupIP("localhost")
synctest.Test(t, func(t *testing.T) {
net.LookupIP("localhost")
})
}What did you see happen?
The process aborts. Which object aborts depends on the resolver path.
Default (cgo) path, verbatim from go1.26.6 darwin/arm64:
fatal error: select on synctest channel from outside bubble
net.acquireThread({...})
/tmp/gomodcache/golang.org/toolchain@v0.0.1-go1.26.6.darwin-arm64/src/net/net.go:813 +0xa0
net.doBlockingWithCtx[...](...)
/tmp/gomodcache/golang.org/toolchain@v0.0.1-go1.26.6.darwin-arm64/src/net/cgo_unix.go:48 +0x58
With GODEBUG=netdns=go it is resolvConf.ch instead:
fatal error: send on synctest channel from outside bubble
net.(*resolverConfig).tryAcquireSema(...)
/tmp/gomodcache/golang.org/toolchain@v0.0.1-go1.26.6.darwin-arm64/src/net/dnsclient_unix.go:430
net.(*resolverConfig).tryUpdate(...)
/tmp/gomodcache/golang.org/toolchain@v0.0.1-go1.26.6.darwin-arm64/src/net/dnsclient_unix.go:396 +0x80
Three package-level channels in net are created inside a sync.Once, so each is associated with the bubble of the first caller to reach it: threadLimit in acquireThread, and resolvConf.ch and nssConfig.ch in their init methods, at dnsclient_unix.go:386 and nss.go:45 at master 72aa6db. nssConfig.ch is not in the traces only because resolvConf.ch aborts first; creating that one eagerly moves the abort into nss.go.
Because the association depends on which caller runs first, one test order runs to completion and another aborts the process. The abort is a fatal error rather than a panic, so no test is reported as failing.
What did you expect to see?
Both orders behaving the same way.
I think this is the case @prattmic raised in #67434, "lazy initialization of a global channel occurring inside the bubble and thus panicking when used outside", and @neild's answer there — "ensure any lazy initialization happens outside the bubble" — applies to two of the three. Both are one-slot semaphores, so they can be created in their var initializers:
var resolvConf = resolverConfig{ch: make(chan struct{}, 1)}
var nssConfig = nsswitchConfig{ch: make(chan struct{}, 1)}I tried that at master 72aa6db. go test -short net passes before and after, the full suite shows no new failures, and with GODEBUG=netdns=go the test above goes from fatal to passing.
threadLimit I left alone. Its capacity comes from rlim.Cur, and a program may lower RLIMIT_NOFILE after package initialization, so eager creation could allow more concurrent getaddrinfo calls than there are descriptors. With the other two changed, the cgo path still aborts in acquireThread.
Activity
gabyhelp commentedon Aug 16, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
net: create resolver semaphore channels eagerly
net: create resolver semaphore channels eagerly
gopherbot commentedon Aug 16, 2026
Change https://go.dev/cl/816200 mentions this issue:
net: create resolver semaphore channels eagerlydr2chase commentedon Aug 26, 2026
@neild
net: create resolver semaphore channels eagerly
net: create resolver semaphore channels eagerly