Commit 22cd58920a for bind
commit 22cd58920ac61cc974eaece4aef00e0b0977f222
Author: Štěpán Balážik <stepan@isc.org>
Date: Wed Sep 16 18:40:34 2026 +0200
Answer a CNAME loop with SERVFAIL
A looping CNAME chain in zone data used to be followed until Python ran
out of stack. Stop as soon as the chain returns to a name it has been
through and answer as named does when it hits max-query-restarts: the
chain collected so far and SERVFAIL.
Assisted-by: Claude:claude-fable-5-1
diff --git a/bin/tests/system/isctest/asyncserver/__init__.py b/bin/tests/system/isctest/asyncserver/__init__.py
index b954c717a5..219ab550d4 100644
--- a/bin/tests/system/isctest/asyncserver/__init__.py
+++ b/bin/tests/system/isctest/asyncserver/__init__.py
@@ -977,6 +977,12 @@ class AsyncDnsServer(_AsyncServer):
if not cname:
return False
+ if qctx.current_qname in qctx.aliases:
+ # CNAME loop
+ qctx.response.set_rcode(dns.rcode.SERVFAIL)
+ return True
+ qctx.aliases.add(qctx.current_qname)
+
qctx.response.set_rcode(dns.rcode.NOERROR)
cname_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, cname.rdtype)
cname_rrset.update(cname)
diff --git a/bin/tests/system/isctest/asyncserver/context.py b/bin/tests/system/isctest/asyncserver/context.py
index 2910add2b4..df8daf4475 100644
--- a/bin/tests/system/isctest/asyncserver/context.py
+++ b/bin/tests/system/isctest/asyncserver/context.py
@@ -68,6 +68,7 @@ class QueryContext:
node: dns.node.Node | None = field(default=None, init=False)
answer: dns.rdataset.Rdataset | None = field(default=None, init=False)
alias: dns.name.Name | None = field(default=None, init=False)
+ aliases: set[dns.name.Name] = field(default_factory=set, init=False)
_initialized_response: dns.message.Message | None = field(default=None, init=False)
_initialized_response_with_zone_data: dns.message.Message | None = field(
default=None, init=False
diff --git a/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db b/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db
index 9256f78d37..7650367735 100644
--- a/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db
+++ b/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db
@@ -3,3 +3,5 @@ example. 300 IN NS ns.example.
ns.example. 300 IN A 10.53.0.1
foo.example. 300 IN CNAME bar.example.
bar.example. 300 IN A 192.0.2.2
+loop1.example. 300 IN CNAME loop2.example.
+loop2.example. 300 IN CNAME loop1.example.
diff --git a/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py b/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py
index 3f0ea842ea..9aed0714e3 100644
--- a/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py
+++ b/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py
@@ -30,3 +30,13 @@ def test_cname_chain_is_followed():
dns.rdatatype.A,
]
assert res.answer[-1][0].to_text() == "192.0.2.2"
+
+
+def test_looping_cname_chain_is_cut_with_servfail():
+ res = query("loop1.example.", "A")
+ isctest.check.servfail(res)
+ assert [rrset.name.to_text() for rrset in res.answer] == [
+ "loop1.example.",
+ "loop2.example.",
+ ]
+ assert all(rrset.rdtype == dns.rdatatype.CNAME for rrset in res.answer)