Commit 73a55d08a4 for bind

commit 73a55d08a430ba4a848f54adfad24507c98eb5dc
Author: OndÅ™ej Surý <ondrej@isc.org>
Date:   Tue Oct 13 16:32:21 2026 +0200

    Test DS lookups at CNAME names from both sides of the cache

    Cover the two behaviours the previous commit relies on: a CNAME answers
    a DS query from the cache whether it was learned from a DS query or from
    an earlier A query, and at a child's apex CNAME the parent's cached
    denial of DS still takes precedence over the CNAME, without an upstream
    fetch.  The unit test gains the fresh/stale combinations of a CNAME and
    a DS at one node.

    Assisted-by: Claude:claude-fable-5-1

diff --git a/bin/tests/system/dnssec_cname_response/ans2/parent.db.in b/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
index cc5ab1d475..b6289e72a9 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
+++ b/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
@@ -11,3 +11,7 @@ ns      IN A    10.53.0.2

 insecure       IN NS   ns.insecure.parent.
 ns.insecure    IN A    10.53.0.2
+
+alias          IN CNAME target.parent.
+primed         IN CNAME target.parent.
+target         IN A    192.0.2.2
diff --git a/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db b/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
index ea8daf590d..6d7472bbb2 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
+++ b/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
@@ -11,3 +11,5 @@ ns              IN A    10.53.0.2
 cname-target    IN A    192.0.2.1
 @               IN MX   10 mail.insecure.parent.
 mail            IN A    10.53.0.2
+alias           IN CNAME cname-target.insecure.parent.
+primed          IN CNAME cname-target.insecure.parent.
diff --git a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
index 28a1fec05a..7ea3e08add 100644
--- a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
+++ b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
@@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import ec
 from dns.rdtypes.dnskeybase import Flag

 import dns.dnssec
+import dns.name
 import dns.rdataclass
 import dns.rdatatype
 import dns.rrset
@@ -124,8 +125,8 @@ EXPECTED_A = dns.rrset.from_text(
 )


-def _query_insecure_parent(ns, qtype):
-    return isctest.query.tcp(isctest.query.create("insecure.parent.", qtype), ns.ip)
+def _query(ns, qname, qtype):
+    return isctest.query.tcp(isctest.query.create(qname, qtype), ns.ip)


 def _check_insecure_cname_chain(res):
@@ -243,26 +244,92 @@ def test_cname_at_insecure_delegation_is_accepted(servers, resolver):
     """
     ns = servers[resolver]

-    res = _query_insecure_parent(ns, "A")
+    res = _query(ns, "insecure.parent.", "A")
     _check_insecure_cname_chain(res)

-    res = _query_insecure_parent(ns, "DS")
+    res = _query(ns, "insecure.parent.", "DS")
     isctest.check.noerror(res)
     isctest.check.empty_answer(res)

-    res = _query_insecure_parent(ns, "NS")
+    res = _query(ns, "insecure.parent.", "NS")
     isctest.check.noerror(res)
     answers = {rrset.rdtype: rrset for rrset in res.answer}
     assert answers.get(dns.rdatatype.CNAME) == EXPECTED_CNAME, res

     # Repeat with a warm cache.
-    res = _query_insecure_parent(ns, "A")
+    res = _query(ns, "insecure.parent.", "A")
     _check_insecure_cname_chain(res)

     assert not ns.log.grep(Re(r"deadlock found resolving 'insecure\.parent"))
     assert not ns.log.grep(Re(r"fetch loop detected resolving 'insecure\.parent"))


+# Ordinary (non-apex) CNAMEs in the signed 'parent.' and the unsigned
+# 'insecure.parent.' zones; the 'primed' ones are cached by an A query first.
+CNAME_DS_CASES = [
+    ("alias.parent.", False),
+    ("primed.parent.", True),
+    ("alias.insecure.parent.", False),
+    ("primed.insecure.parent.", True),
+]
+
+
+@pytest.mark.parametrize("resolver", RESOLVERS)
+@pytest.mark.parametrize(
+    "qname,primed", CNAME_DS_CASES, ids=[qname for qname, _ in CNAME_DS_CASES]
+)
+def test_ds_query_at_cname_is_answered_from_cache(servers, resolver, qname, primed):
+    """
+    A cached CNAME answers DS queries like any other type: a DS query at an
+    uncached name is fetched once and then served from the cache, and a CNAME
+    already cached by an A query is used right away (GL#6455).
+    """
+    ns = servers[resolver]
+    name = qname.rstrip(".").replace(".", r"\.")
+    fetch = Re(f"fetch: {name}/DS")
+
+    if primed:
+        res = _query(ns, qname, "A")
+        isctest.check.noerror(res)
+        assert res.answer[0].rdtype == dns.rdatatype.CNAME, res
+
+    for _ in range(2):
+        res = _query(ns, qname, "DS")
+        isctest.check.noerror(res)
+        assert res.answer, res
+        assert res.answer[0].rdtype == dns.rdatatype.CNAME, res
+        assert res.answer[0].name == dns.name.from_text(qname), res
+
+    fetches = len(ns.log.grep(fetch))
+    assert fetches == (0 if primed else 1), f"{qname}/DS fetched {fetches} times"
+
+
+@pytest.mark.parametrize("resolver", RESOLVERS)
+def test_ds_denial_beside_apex_cname_is_served_from_cache(servers, resolver):
+    """
+    ans2 answers "insecure.parent/A" with a CNAME to
+    "cname-target.insecure.parent" and every other query normally.
+    Validating that chain caches the nonexistence of "insecure.parent/DS".
+    The cached denial, not the CNAME, must answer a later DS query, without
+    another upstream fetch (GL#6455).
+    """
+    ns = servers[resolver]
+    fetch = Re(r"fetch: insecure\.parent/DS")
+
+    ns.rndc("flush")
+
+    # Validating the unsigned CNAME chain caches the parent's denial of DS.
+    res = _query(ns, "insecure.parent.", "A")
+    _check_insecure_cname_chain(res)
+    fetched = len(ns.log.grep(fetch))
+
+    res = _query(ns, "insecure.parent.", "DS")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+    isctest.check.empty_answer(res)
+    assert len(ns.log.grep(fetch)) == fetched, "the DS query was fetched upstream"
+
+
 def test_apex_cname_coexists_with_other_types(ns3):
     """
     Caching an apex CNAME must preserve an existing MX RRset.
@@ -283,13 +350,13 @@ def test_apex_cname_coexists_with_other_types(ns3):
     # Cache MX first; a cached CNAME would answer the MX query via the alias.
     ns3.rndc("flushtree insecure.parent")

-    res = _query_insecure_parent(ns3, "MX")
+    res = _query(ns3, "insecure.parent.", "MX")
     check_mx(res)

-    res = _query_insecure_parent(ns3, "A")
+    res = _query(ns3, "insecure.parent.", "A")
     _check_insecure_cname_chain(res)

-    res = _query_insecure_parent(ns3, "MX")
+    res = _query(ns3, "insecure.parent.", "MX")
     check_mx(res)

     with ns3.watch_log_from_here() as watcher:
diff --git a/tests/dns/qpdb_test.c b/tests/dns/qpdb_test.c
index 58bccbafaf..063284bc95 100644
--- a/tests/dns/qpdb_test.c
+++ b/tests/dns/qpdb_test.c
@@ -451,8 +451,8 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
 			.expected_type = cname,
 			.expected_stale = true,
 		},
-		/* A lone CNAME answers ordinary types, including NS, but not
-		   DS. */
+		/* A lone CNAME answers every ordinary type, NS and DS
+		   included. */
 		{
 			.type1 = cname,
 			.rank1 = fresh,
@@ -483,8 +483,7 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
 			.expected_type = cname,
 			.expected_stale = false,
 		},
-		/* A lone stale CNAME answers ordinary types, including NS, but
-		   not DS. */
+		/* A lone stale CNAME does the same. */
 		{
 			.type1 = cname,
 			.rank1 = stale,
@@ -536,6 +535,38 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
 			.expected_type = ns,
 			.expected_stale = false,
 		},
+		/* DS beside a CNAME follows the same rules as any other type:
+		   fresher wins, the requested type wins a tie. */
+		{
+			.type1 = cname,
+			.rank1 = fresh,
+			.type2 = ds,
+			.rank2 = stale,
+			.qtype = ds,
+			.expected_result = DNS_R_CNAME,
+			.expected_type = cname,
+			.expected_stale = false,
+		},
+		{
+			.type1 = cname,
+			.rank1 = stale,
+			.type2 = ds,
+			.rank2 = fresh,
+			.qtype = ds,
+			.expected_result = ISC_R_SUCCESS,
+			.expected_type = ds,
+			.expected_stale = false,
+		},
+		{
+			.type1 = cname,
+			.rank1 = stale,
+			.type2 = ds,
+			.rank2 = stale,
+			.qtype = ds,
+			.expected_result = ISC_R_SUCCESS,
+			.expected_type = ds,
+			.expected_stale = true,
+		},
 		/* DS does not hide a CNAME from ordinary queries. */
 		{
 			.type1 = cname,