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.