mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 03:55:21 -04:00
plugin/route53: don't hold zMu across zone Lookup (#8601)
This commit is contained in:
committed by
GitHub
parent
f44a91377a
commit
92129ace31
131
plugin/route53/lock_repro_test.go
Normal file
131
plugin/route53/lock_repro_test.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package route53
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
"github.com/coredns/coredns/plugin"
|
||||||
|
"github.com/coredns/coredns/plugin/file"
|
||||||
|
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||||
|
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||||
|
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||||
|
"github.com/coredns/coredns/plugin/test"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestServeDNSDoesNotHoldLockDuringUpstreamLookup reproduces the same bug
|
||||||
|
// class that plugin/azure fixed in PR #8447 ("plugin/azure: don't hold zMu
|
||||||
|
// across zone Lookup"), but for plugin/route53, where it is still present
|
||||||
|
// in route53.go's ServeDNS:
|
||||||
|
//
|
||||||
|
// h.zMu.RLock()
|
||||||
|
// m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname)
|
||||||
|
// h.zMu.RUnlock()
|
||||||
|
//
|
||||||
|
// route53 zones are created with newZ.Upstream = h.upstream (see
|
||||||
|
// updateZones), so Lookup can chase a CNAME whose target is outside the
|
||||||
|
// zone via Upstream.Lookup, which re-enters the plugin chain and can block
|
||||||
|
// on real network I/O -- all while h.zMu.RLock() is held. Go's
|
||||||
|
// sync.RWMutex blocks new RLock() calls once a Lock() is pending, so one
|
||||||
|
// slow/hanging upstream lookup here stalls updateZones' periodic
|
||||||
|
// h.zMu.Lock() call, and every other query queues up behind that pending
|
||||||
|
// writer -- even for unrelated zones. This is also the direct ancestor of
|
||||||
|
// the still-open github.com/coredns/coredns/issues/6664: because Lookup
|
||||||
|
// runs *inside* the locked section, a panic inside it is caught by
|
||||||
|
// CoreDNS's per-request recover() in core/dnsserver/server.go but never
|
||||||
|
// reaches h.zMu.RUnlock(), permanently wedging the mutex.
|
||||||
|
func TestServeDNSDoesNotHoldLockDuringUpstreamLookup(t *testing.T) {
|
||||||
|
const timeout = 5 * time.Second
|
||||||
|
|
||||||
|
entered := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
var releaseOnce sync.Once
|
||||||
|
releaseHandler := func() { releaseOnce.Do(func() { close(release) }) }
|
||||||
|
defer releaseHandler()
|
||||||
|
|
||||||
|
cfg := &dnsserver.Config{
|
||||||
|
Zone: ".",
|
||||||
|
Plugin: []plugin.Plugin{
|
||||||
|
func(plugin.Handler) plugin.Handler {
|
||||||
|
return plugin.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||||
|
close(entered)
|
||||||
|
<-release
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = []dns.RR{test.A("external.target. 300 IN A 5.6.7.8")}
|
||||||
|
w.WriteMsg(m)
|
||||||
|
return dns.RcodeSuccess, nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
srv, err := dnsserver.NewServer("", []*dnsserver.Config{cfg})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(context.Background(), dnsserver.Key{}, srv)
|
||||||
|
|
||||||
|
newZ := file.NewZone("example.org.", "")
|
||||||
|
newZ.Upstream = upstream.New()
|
||||||
|
for _, rr := range []string{
|
||||||
|
"example.org. 300 IN SOA ns1.example.org. hostmaster.example.org. 1 3600 300 2419200 300",
|
||||||
|
"cname-ext.example.org. 300 IN CNAME external.target.",
|
||||||
|
} {
|
||||||
|
r, _ := dns.NewRR(rr)
|
||||||
|
newZ.Insert(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &Route53{
|
||||||
|
Fall: fall.Zero,
|
||||||
|
zoneNames: []string{"example.org."},
|
||||||
|
zones: zones{"example.org.": {{id: "Z1", dns: "example.org.", z: newZ}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(dns.Msg)
|
||||||
|
req.SetQuestion("cname-ext.example.org.", dns.TypeA)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||||
|
h.ServeDNS(ctx, rec, req)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-entered:
|
||||||
|
// The query reached the upstream handler and is now blocked on
|
||||||
|
// release, with h.zMu.RLock() (claimed) held for the duration if
|
||||||
|
// the bug is present.
|
||||||
|
case <-time.After(timeout):
|
||||||
|
t.Fatal("query never reached the upstream handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
lockAcquired := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
// Stands in for the periodic zone swap updateZones performs.
|
||||||
|
h.zMu.Lock()
|
||||||
|
h.zones["example.org."][0].z = file.NewZone("example.org.", "")
|
||||||
|
h.zMu.Unlock()
|
||||||
|
close(lockAcquired)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-lockAcquired:
|
||||||
|
// h.zMu.Lock() was not blocked by the in-flight upstream lookup:
|
||||||
|
// the fix is in place.
|
||||||
|
case <-time.After(timeout):
|
||||||
|
t.Fatal("h.zMu.Lock() blocked while a query awaited a slow upstream lookup (bug reproduced)")
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseHandler()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(timeout):
|
||||||
|
t.Fatal("ServeDNS did not complete after the upstream handler was released")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,9 +121,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
|
|||||||
m.Authoritative = true
|
m.Authoritative = true
|
||||||
var result file.Result
|
var result file.Result
|
||||||
for _, hostedZone := range z {
|
for _, hostedZone := range z {
|
||||||
|
// Only the zone pointer itself needs to be guarded against a
|
||||||
|
// concurrent swap in updateZones; Lookup can run unlocked since it
|
||||||
|
// may block for a while resolving external names via upstream, and
|
||||||
|
// a panic inside it must not leave zMu permanently locked.
|
||||||
h.zMu.RLock()
|
h.zMu.RLock()
|
||||||
m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname)
|
hz := hostedZone.z
|
||||||
h.zMu.RUnlock()
|
h.zMu.RUnlock()
|
||||||
|
m.Answer, m.Ns, m.Extra, result = hz.Lookup(ctx, state, qname)
|
||||||
|
|
||||||
// Take the answer if it's non-empty OR if there is another
|
// Take the answer if it's non-empty OR if there is another
|
||||||
// record type exists for this name (NODATA).
|
// record type exists for this name (NODATA).
|
||||||
|
|||||||
Reference in New Issue
Block a user