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>
This commit is contained in:
Pavel Lazureykis
2026-07-09 00:44:16 -04:00
committed by GitHub
parent ea99084dec
commit 9bf5de8e92
2 changed files with 64 additions and 6 deletions

View File

@@ -6,8 +6,10 @@ package hosts
import (
"net"
"os"
"reflect"
"strings"
"sync"
"testing"
"github.com/coredns/coredns/plugin"
@@ -241,3 +243,50 @@ func TestHostCacheModification(t *testing.T) {
}
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()
}