Files
coredns/plugin/hosts/hostsfile.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
6.6 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.
// This file is a modified version of net/hosts.go from the golang repo
package hosts
import (
"bufio"
"bytes"
"io"
"net"
"os"
"strings"
"sync"
"time"
"github.com/coredns/coredns/plugin"
)
// parseIP calls discards any v6 zone info, before calling net.ParseIP.
func parseIP(addr string) net.IP {
if i := strings.Index(addr, "%"); i >= 0 {
// discard ipv6 zone
addr = addr[0:i]
}
return net.ParseIP(addr)
}
type options struct {
// automatically generate IP to Hostname PTR entries
// for host entries we parse
autoReverse bool
// The TTL of the record we generate
ttl uint32
// The time between two reload of the configuration
reload time.Duration
}
func newOptions() *options {
return &options{
autoReverse: true,
ttl: 3600,
reload: 5 * time.Second,
}
}
// Map contains the IPv4/IPv6 and reverse mapping.
type Map struct {
// Key for the list of literal IP addresses must be a FQDN lowercased host name.
name4 map[string][]net.IP
name6 map[string][]net.IP
// Wildcard owner names (e.g. *.example.com.) map to IP addresses.
wildName4 map[string][]net.IP
wildName6 map[string][]net.IP
// Key for the list of host names must be a literal IP address
// including IPv6 address without zone identifier.
// We don't support old-classful IP address notation.
addr map[string][]string
}
func newMap() *Map {
return &Map{
name4: make(map[string][]net.IP),
name6: make(map[string][]net.IP),
wildName4: make(map[string][]net.IP),
wildName6: make(map[string][]net.IP),
addr: make(map[string][]string),
}
}
// Len returns the total number of addresses in the hostmap, this includes V4/V6 and any reverse addresses.
func (h *Map) Len() int {
l := 0
for _, v4 := range h.name4 {
l += len(v4)
}
for _, v6 := range h.name6 {
l += len(v6)
}
for _, v4 := range h.wildName4 {
l += len(v4)
}
for _, v6 := range h.wildName6 {
l += len(v6)
}
for _, a := range h.addr {
l += len(a)
}
return l
}
// Hostsfile contains known host entries.
type Hostsfile struct {
sync.RWMutex
// list of zones we are authoritative for
Origins []string
// hosts maps for lookups
hmap *Map
// inline saves the hosts file that is inlined in a Corefile.
inline *Map
// path to the hosts file
path string
// mtime and size are only read and modified by a single goroutine
mtime time.Time
size int64
options *options
}
// readHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile.
func (h *Hostsfile) readHosts() {
file, err := os.Open(h.path)
if err != nil {
// We already log a warning if the file doesn't exist or can't be opened on setup. No need to return the error here.
return
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return
}
h.RLock()
size := h.size
mtime := h.mtime
h.RUnlock()
if mtime.Equal(stat.ModTime()) && size == stat.Size() {
return
}
newMap := h.parse(file)
log.Debugf("Parsed hosts file into %d entries", newMap.Len())
h.Lock()
h.hmap = newMap
// Update the data cache.
h.mtime = stat.ModTime()
h.size = stat.Size()
hostsEntries.WithLabelValues(h.path).Set(float64(h.inline.Len() + h.hmap.Len()))
hostsReloadTime.Set(float64(stat.ModTime().UnixNano()) / 1e9)
h.Unlock()
}
func (h *Hostsfile) initInline(inline []string) {
if len(inline) == 0 {
return
}
h.inline = h.parse(strings.NewReader(strings.Join(inline, "\n")))
}
// Parse reads the hostsfile and populates the byName and addr maps.
func (h *Hostsfile) parse(r io.Reader) *Map {
hmap := newMap()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Bytes()
if i := bytes.Index(line, []byte{'#'}); i >= 0 {
// Discard comments.
line = line[0:i]
}
f := bytes.Fields(line)
if len(f) < 2 {
continue
}
addr := parseIP(string(f[0]))
if addr == nil {
continue
}
var family int
if addr.To4() != nil {
family = 1
} else {
family = 2
}
for i := 1; i < len(f); i++ {
name := plugin.Name(string(f[i])).Normalize()
if plugin.Zones(h.Origins).Matches(name) == "" {
// name is not in Origins
continue
}
if isWildcardName(name) {
switch family {
case 1:
hmap.wildName4[name] = append(hmap.wildName4[name], addr)
case 2:
hmap.wildName6[name] = append(hmap.wildName6[name], addr)
}
continue
}
switch family {
case 1:
hmap.name4[name] = append(hmap.name4[name], addr)
case 2:
hmap.name6[name] = append(hmap.name6[name], addr)
default:
continue
}
if !h.options.autoReverse {
continue
}
hmap.addr[addr.String()] = append(hmap.addr[addr.String()], name)
}
}
return hmap
}
func (h *Hostsfile) lookupStaticHostLocked(m, wild map[string][]net.IP, host string) []net.IP {
if ips, ok := m[host]; ok {
ipsCp := make([]net.IP, len(ips))
copy(ipsCp, ips)
return ipsCp
}
if pattern := replaceWithAsteriskLabel(host); pattern != "" {
if ips, ok := wild[pattern]; ok {
ipsCp := make([]net.IP, len(ips))
copy(ipsCp, ips)
return ipsCp
}
}
return nil
}
func (h *Hostsfile) lookupStaticHostFamily(host string, v4 bool) []net.IP {
host = strings.ToLower(host)
h.RLock()
defer h.RUnlock()
// h.hmap and h.inline must be read under the lock: readHosts swaps h.hmap
// under h.Lock() on every reload.
var ip1, ip2 []net.IP
if v4 {
ip1 = h.lookupStaticHostLocked(h.hmap.name4, h.hmap.wildName4, host)
ip2 = h.lookupStaticHostLocked(h.inline.name4, h.inline.wildName4, host)
} else {
ip1 = h.lookupStaticHostLocked(h.hmap.name6, h.hmap.wildName6, host)
ip2 = h.lookupStaticHostLocked(h.inline.name6, h.inline.wildName6, host)
}
return append(ip1, ip2...)
}
// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
return h.lookupStaticHostFamily(host, true)
}
// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
return h.lookupStaticHostFamily(host, false)
}
// LookupStaticAddr looks up the hosts for the given address from the hosts file.
func (h *Hostsfile) LookupStaticAddr(addr string) []string {
addr = parseIP(addr).String()
if addr == "" {
return nil
}
h.RLock()
defer h.RUnlock()
hosts1 := h.hmap.addr[addr]
hosts2 := h.inline.addr[addr]
if len(hosts1) == 0 && len(hosts2) == 0 {
return nil
}
hostsCp := make([]string, len(hosts1)+len(hosts2))
copy(hostsCp, hosts1)
copy(hostsCp[len(hosts1):], hosts2)
return hostsCp
}