From 458c755e4013d43cd2ee3c5284a475f4a38054a1 Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Sun, 16 Aug 2026 19:10:36 +0300 Subject: [PATCH] net: create resolver semaphore channels eagerly A lookup from outside a testing/synctest bubble, after net's resolver has been used inside one, aborts with "send on synctest channel from outside bubble". One test order runs to completion and another aborts the process, and a fatal error reports no failing test. resolverConfig.ch and nsswitchConfig.ch are created inside their initOnce.Do, so each is associated with the bubble of the first caller to reach the resolver. Both are used only as one-slot semaphores, so they are now created in their package-level var initializers, where no bubble exists. That removes the ordering dependence at no cost. threadLimit in net.go has the same shape but is left alone: its capacity derives from rlim.Cur, and a program may lower RLIMIT_NOFILE after package initialization, so eager creation could permit more concurrent getaddrinfo calls than there are descriptors. Updates #80903 --- src/net/dnsclient_unix.go | 11 ++++++----- src/net/dnsclient_unix_test.go | 27 +++++++++++++++++++++++++++ src/net/nss.go | 6 ++++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go index 6e749882c25c6a..ed9dc762fdecc7 100644 --- a/src/net/dnsclient_unix.go +++ b/src/net/dnsclient_unix.go @@ -363,7 +363,12 @@ type resolverConfig struct { dnsConfig atomic.Pointer[dnsConfig] // parsed resolv.conf structure used in lookups } -var resolvConf resolverConfig +// ch is created here rather than in init: a channel created inside a synctest +// bubble stays associated with it, and operating on such a channel from +// outside the bubble is a fatal error. +var resolvConf = resolverConfig{ + ch: make(chan struct{}, 1), +} func getSystemDNSConfig() *dnsConfig { return getSystemDNSConfigNamed("/etc/resolv.conf") @@ -380,10 +385,6 @@ func (conf *resolverConfig) init() { // resolv.conf twice the first time. conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf")) conf.lastChecked = time.Now() - - // Prepare ch so that only one update of resolverConfig may - // run at once. - conf.ch = make(chan struct{}, 1) } // distantFuture is a sentinel time used for tests to signal that diff --git a/src/net/dnsclient_unix_test.go b/src/net/dnsclient_unix_test.go index fc2eeb6acb23f1..9fbe2453e8e97b 100644 --- a/src/net/dnsclient_unix_test.go +++ b/src/net/dnsclient_unix_test.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "internal/testenv" "maps" "os" "path" @@ -21,6 +22,7 @@ import ( "sync" "sync/atomic" "testing" + "testing/synctest" "time" "golang.org/x/net/dns/dnsmessage" @@ -2881,3 +2883,28 @@ func TestEmptyResolvConfReplacedWithConfHaingNameservers(t *testing.T) { t.Fatal("resolv.conf was not re-loaded") } } + +// Run in a subprocess: a bubble violation is fatal, not a recoverable panic. +func TestResolverConfigSemaphoresNotBubbled(t *testing.T) { + if os.Getenv("GO_NET_TEST_BUBBLE_HELPER") == "1" { + synctest.Test(t, func(t *testing.T) { + getSystemDNSConfig() + getSystemNSS() + }) + // getSystemDNSConfig would skip the send on "options no-reload". + if resolvConf.tryAcquireSema() { + resolvConf.releaseSema() + } + if nssConfig.tryAcquireSema() { + nssConfig.releaseSema() + } + return + } + testenv.MustHaveExec(t) + cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+t.Name()+"$", "-test.count=1") + cmd = testenv.CleanCmdEnv(cmd) + cmd.Env = append(cmd.Env, "GO_NET_TEST_BUBBLE_HELPER=1") + if out, err := cmd.CombinedOutput(); err != nil { + t.Errorf("helper failed: %v\n%s", err, out) + } +} diff --git a/src/net/nss.go b/src/net/nss.go index 092b515cc7d01e..fdd66f725b52ad 100644 --- a/src/net/nss.go +++ b/src/net/nss.go @@ -16,7 +16,10 @@ const ( nssConfigPath = "/etc/nsswitch.conf" ) -var nssConfig nsswitchConfig +// ch is created here rather than in init; see resolvConf. +var nssConfig = nsswitchConfig{ + ch: make(chan struct{}, 1), +} type nsswitchConfig struct { initOnce sync.Once // guards init of nsswitchConfig @@ -42,7 +45,6 @@ func getSystemNSS() *nssConf { func (conf *nsswitchConfig) init() { conf.nssConf = parseNSSConfFile("/etc/nsswitch.conf") conf.lastChecked = time.Now() - conf.ch = make(chan struct{}, 1) } // tryUpdate tries to update conf.