net: lazily created semaphore channels associate with a synctest bubble
- Dominant language
- Go
- Stars
- 139k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
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 72aa6db7, 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:
```go
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:
```go
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 72aa6db7. `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:
```go
var resolvConf = resolverConfig{ch: make(chan struct{}, 1)}
var nssConfig = nsswitchConfig{ch: make(chan struct{}, 1)}
```
I tried that at master 72aa6db7. `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`.
Contributor guide
Research direction
Start with net/net.go, net/dnsclient_unix.go, and net/nss.go, focusing on the package-level semaphore channels created during lazy initialization. Run the synctest reproducer in both call orders and verify the cgo and GODEBUG=netdns=go paths. Done means both orders complete without a fatal synctest-channel error while preserving the threadLimit behavior described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100