Files
coredns/plugin/hosts/hostsfile_test.go
Pavel Lazureykis 9bf5de8e92 plugin/hosts: fix data race between lookups and reload (#8253)
LookupStaticHostV4/V6 dereferenced h.hmap and h.inline as call arguments,
which are evaluated before lookupStaticHostFamily takes the read lock. The
reload path (readHosts) swaps h.hmap under h.Lock() on every reload, so the
field read raced the swap for every A/AAAA lookup. This was introduced when
wildcard support (#8185) refactored these methods to pass the maps as
parameters; LookupStaticAddr still reads the fields inside the lock and was
unaffected.

Read h.hmap/h.inline inside the RLock by selecting the address family with a
bool instead of passing pre-dereferenced maps.

Also read h.mtime under the existing RLock in readHosts: it was read without a
lock while the reload writes it under h.Lock(), and readHosts runs from both
the OnStartup handler and the reload ticker goroutine.

Both races are confirmed by go test -race.

Signed-off-by: Pavel Lazureykis <pavel@lazureykis.dev>
2026-07-08 21:44:16 -07:00

293 lines
7.3 KiB
Go

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hosts
import (
"net"
"os"
"reflect"
"strings"
"sync"
"testing"
"github.com/coredns/coredns/plugin"
)
func testHostsfile(file string) *Hostsfile {
h := &Hostsfile{
Origins: []string{"."},
hmap: newMap(),
inline: newMap(),
options: newOptions(),
}
h.hmap = h.parse(strings.NewReader(file))
return h
}
type staticHostEntry struct {
in string
v4 []string
v6 []string
}
var (
hosts = `255.255.255.255 broadcasthost
127.0.0.2 odin
127.0.0.3 odin # inline comment
::2 odin
127.1.1.1 thor
# aliases
127.1.1.2 ullr ullrhost
fe80::1%lo0 localhost
# Bogus entries that must be ignored.
123.123.123 loki
321.321.321.321`
singlelinehosts = `127.0.0.2 odin`
ipv4hosts = `# See https://tools.ietf.org/html/rfc1123.
#
# internet address and host name
127.0.0.1 localhost # inline comment separated by tab
127.0.0.2 localhost # inline comment separated by space
# internet address, host name and aliases
127.0.0.3 localhost localhost.localdomain`
ipv6hosts = `# See https://tools.ietf.org/html/rfc5952, https://tools.ietf.org/html/rfc4007.
# internet address and host name
::1 localhost # inline comment separated by tab
fe80:0000:0000:0000:0000:0000:0000:0001 localhost # inline comment separated by space
# internet address with zone identifier and host name
fe80:0000:0000:0000:0000:0000:0000:0002%lo0 localhost
# internet address, host name and aliases
fe80::3%lo0 localhost localhost.localdomain`
casehosts = `127.0.0.1 PreserveMe PreserveMe.local
::1 PreserveMe PreserveMe.local`
)
var lookupStaticHostTests = []struct {
file string
ents []staticHostEntry
}{
{
hosts,
[]staticHostEntry{
{"odin.", []string{"127.0.0.2", "127.0.0.3"}, []string{"::2"}},
{"thor.", []string{"127.1.1.1"}, []string{}},
{"ullr.", []string{"127.1.1.2"}, []string{}},
{"ullrhost.", []string{"127.1.1.2"}, []string{}},
{"localhost.", []string{}, []string{"fe80::1"}},
},
},
{
singlelinehosts, // see golang.org/issue/6646
[]staticHostEntry{
{"odin.", []string{"127.0.0.2"}, []string{}},
},
},
{
ipv4hosts,
[]staticHostEntry{
{"localhost.", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}},
{"localhost.localdomain.", []string{"127.0.0.3"}, []string{}},
},
},
{
ipv6hosts,
[]staticHostEntry{
{"localhost.", []string{}, []string{"::1", "fe80::1", "fe80::2", "fe80::3"}},
{"localhost.localdomain.", []string{}, []string{"fe80::3"}},
},
},
{
casehosts,
[]staticHostEntry{
{"PreserveMe.", []string{"127.0.0.1"}, []string{"::1"}},
{"PreserveMe.local.", []string{"127.0.0.1"}, []string{"::1"}},
},
},
}
func TestLookupStaticHost(t *testing.T) {
for _, tt := range lookupStaticHostTests {
h := testHostsfile(tt.file)
for _, ent := range tt.ents {
testStaticHost(t, ent, h)
}
}
}
func testStaticHost(t *testing.T, ent staticHostEntry, h *Hostsfile) {
t.Helper()
ins := []string{ent.in, plugin.Name(ent.in).Normalize(), strings.ToLower(ent.in), strings.ToUpper(ent.in)}
for k, in := range ins {
addrsV4 := h.LookupStaticHostV4(in)
if len(addrsV4) != len(ent.v4) {
t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4)
}
for i, v4 := range addrsV4 {
if v4.String() != ent.v4[i] {
t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4)
}
}
addrsV6 := h.LookupStaticHostV6(in)
if len(addrsV6) != len(ent.v6) {
t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6)
}
for i, v6 := range addrsV6 {
if v6.String() != ent.v6[i] {
t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6)
}
}
}
}
type staticIPEntry struct {
in string
out []string
}
var lookupStaticAddrTests = []struct {
file string
ents []staticIPEntry
}{
{
hosts,
[]staticIPEntry{
{"255.255.255.255", []string{"broadcasthost."}},
{"127.0.0.2", []string{"odin."}},
{"127.0.0.3", []string{"odin."}},
{"::2", []string{"odin."}},
{"127.1.1.1", []string{"thor."}},
{"127.1.1.2", []string{"ullr.", "ullrhost."}},
{"fe80::1", []string{"localhost."}},
},
},
{
singlelinehosts, // see golang.org/issue/6646
[]staticIPEntry{
{"127.0.0.2", []string{"odin."}},
},
},
{
ipv4hosts, // see golang.org/issue/8996
[]staticIPEntry{
{"127.0.0.1", []string{"localhost."}},
{"127.0.0.2", []string{"localhost."}},
{"127.0.0.3", []string{"localhost.", "localhost.localdomain."}},
},
},
{
ipv6hosts, // see golang.org/issue/8996
[]staticIPEntry{
{"::1", []string{"localhost."}},
{"fe80::1", []string{"localhost."}},
{"fe80::2", []string{"localhost."}},
{"fe80::3", []string{"localhost.", "localhost.localdomain."}},
},
},
{
casehosts, // see golang.org/issue/12806
[]staticIPEntry{
{"127.0.0.1", []string{"PreserveMe.", "PreserveMe.local."}},
{"::1", []string{"PreserveMe.", "PreserveMe.local."}},
},
},
}
func TestLookupStaticAddr(t *testing.T) {
for _, tt := range lookupStaticAddrTests {
h := testHostsfile(tt.file)
for _, ent := range tt.ents {
testStaticAddr(t, ent, h)
}
}
}
func testStaticAddr(t *testing.T, ent staticIPEntry, h *Hostsfile) {
t.Helper()
hosts := h.LookupStaticAddr(ent.in)
for i := range ent.out {
ent.out[i] = plugin.Name(ent.out[i]).Normalize()
}
if !reflect.DeepEqual(hosts, ent.out) {
t.Errorf("%s, lookupStaticAddr(%s) = %v; want %v", h.path, ent.in, hosts, h)
}
}
func TestHostCacheModification(t *testing.T) {
// Ensure that programs can't modify the internals of the host cache.
// See https://github.com/golang/go/issues/14212.
h := testHostsfile(ipv4hosts)
ent := staticHostEntry{"localhost.", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}}
testStaticHost(t, ent, h)
// Modify the addresses return by lookupStaticHost.
addrs := h.LookupStaticHostV6(ent.in)
for i := range addrs {
addrs[i] = net.IPv4zero
}
testStaticHost(t, ent, h)
h = testHostsfile(ipv6hosts)
entip := staticIPEntry{"::1", []string{"localhost."}}
testStaticAddr(t, entip, h)
// Modify the hosts return by lookupStaticAddr.
hosts := h.LookupStaticAddr(entip.in)
for i := range hosts {
hosts[i] += "junk"
}
testStaticAddr(t, entip, h)
}
// TestLookupStaticHostReloadRace exercises concurrent A/AAAA lookups against a
// reload that swaps h.hmap. Before the fix, LookupStaticHostV4/V6 dereferenced
// h.hmap/h.inline as call arguments (outside the RLock), racing the swap that
// readHosts performs under h.Lock(). Run with -race.
func TestLookupStaticHostReloadRace(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "hosts")
if err != nil {
t.Fatal(err)
}
if _, err := f.WriteString("127.0.0.1 example.org\n::1 example.org\n"); err != nil {
t.Fatal(err)
}
f.Close()
h := &Hostsfile{
Origins: []string{"."},
hmap: newMap(),
inline: newMap(),
options: newOptions(),
path: f.Name(),
}
var wg sync.WaitGroup
// Reloader: force a re-parse + h.hmap swap on every iteration.
wg.Go(func() {
for range 1000 {
h.Lock()
h.size = 0
h.Unlock()
h.readHosts()
}
})
// Readers: concurrent A/AAAA lookups.
for range 4 {
wg.Go(func() {
for range 1000 {
h.LookupStaticHostV4("example.org.")
h.LookupStaticHostV6("example.org.")
}
})
}
wg.Wait()
}