fix(file): fix data race in tree Elem.Name (#7574)

Eagerly set name in newElem and make Name() read-only to avoid
racy lazy writes under concurrent lookups. Add tests for empty-name
comparisons and concurrent access to Less/Name(). In addition,
regression tests to CloudDNS plugin.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
This commit is contained in:
Ville Vesilehto
2025-09-29 05:49:47 +03:00
committed by GitHub
parent eafc352f58
commit 70fb03f711
4 changed files with 217 additions and 2 deletions

View File

@@ -12,6 +12,8 @@ type Elem struct {
func newElem(rr dns.RR) *Elem {
e := Elem{m: make(map[uint16][]dns.RR)}
e.m[rr.Header().Rrtype] = []dns.RR{rr}
// Eagerly set the cached owner name to avoid racy lazy writes later.
e.name = rr.Header().Name
return &e
}
@@ -56,12 +58,12 @@ func (e *Elem) All() []dns.RR {
// Name returns the name for this node.
func (e *Elem) Name() string {
// Read-only: name is eagerly set in newElem and should not be mutated here.
if e.name != "" {
return e.name
}
for _, rrs := range e.m {
e.name = rrs[0].Header().Name
return e.name
return rrs[0].Header().Name
}
return ""
}

View File

@@ -0,0 +1,41 @@
package tree
import (
"testing"
"github.com/miekg/dns"
)
// Test that Name() falls back to reading from the stored RRs when the cached name is empty.
func TestElemName_FallbackWhenCachedEmpty(t *testing.T) {
rr, err := dns.NewRR("a.example. 3600 IN A 1.2.3.4")
if err != nil {
t.Fatalf("failed to create RR: %v", err)
}
// Build via newElem to ensure m is populated
e := newElem(rr)
got := e.Name()
want := "a.example."
if got != want {
t.Fatalf("unexpected name; want %q, got %q", want, got)
}
// clear the cached name
e.name = ""
got = e.Name()
want = "a.example."
if got != want {
t.Fatalf("unexpected name; want %q, got %q", want, got)
}
// clear the map
e.m = make(map[uint16][]dns.RR, 0)
got = e.Name()
want = ""
if got != want {
t.Fatalf("unexpected name after clearing RR map; want %q, got %q", want, got)
}
}

View File

@@ -3,7 +3,10 @@ package tree
import (
"sort"
"strings"
"sync"
"testing"
"github.com/miekg/dns"
)
type set []string
@@ -78,3 +81,41 @@ Tests:
}
}
}
func TestLess_EmptyVsName(t *testing.T) {
if d := less("", "a."); d >= 0 {
t.Fatalf("expected < 0, got %d", d)
}
if d := less("a.", ""); d <= 0 {
t.Fatalf("expected > 0, got %d", d)
}
}
func TestLess_EmptyVsEmpty(t *testing.T) {
if d := less("", ""); d != 0 {
t.Fatalf("expected 0, got %d", d)
}
}
// Test that concurrent calls to Less (which calls Elem.Name) do not race or panic.
// See issue #7561 for reference.
func TestLess_ConcurrentNameAccess(t *testing.T) {
rr, err := dns.NewRR("a.example. 3600 IN A 1.2.3.4")
if err != nil {
t.Fatalf("failed to create RR: %v", err)
}
e := newElem(rr)
const n = 200
var wg sync.WaitGroup
wg.Add(n)
for range n {
go func() {
defer wg.Done()
// Compare the same name repeatedly; previously this could race due to lazy Name() writes.
_ = Less(e, "a.example.")
_ = e.Name()
}()
}
wg.Wait()
}