golang / golang/go

net: lazily created semaphore channels associate with a synctest bubble

Open
#80,903 3 comments 0 reactions 0 assignees View on GitHub
NeedsInvestigation
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.