Commit 737831182b for bind

commit 737831182bb09e189079a585f8d16911f96fdbbf
Author: OndÅ™ej Surý <ondrej@isc.org>
Date:   Tue Nov 10 12:12:27 2026 +0100

    Clean up tests that relied on standalone RRSIG queries

    With RRSIG queries rejected up front, several checks kept passing
    without reaching the code they were written for. The malformed RRSIG
    regression test now makes the resolver fetch the record with an ANY
    query, and the resolver test looks for the leftover additional RRSIG
    in a cache dump, which also confirms the signature was cached in the
    first place.

    The remaining checks covered only the removed handling of RRSIG as a
    subset of ANY, or repeated the FORMERR assertion, and are removed.

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

diff --git a/bin/tests/system/dnssec/ns3/secure.example.db.in b/bin/tests/system/dnssec/ns3/secure.example.db.in
index 4d66e5150a..e0c11168b7 100644
--- a/bin/tests/system/dnssec/ns3/secure.example.db.in
+++ b/bin/tests/system/dnssec/ns3/secure.example.db.in
@@ -37,9 +37,6 @@ ns2.insecure		A	10.53.0.2
 nosoa 			NS	ns6.nosoa
 ns6.nosoa		A	10.53.0.6

-normalthenrrsig		A	10.0.0.28
-rrsigonly		A	10.0.0.29
-
 cnameandkey		CNAME	@
 dnameandkey		DNAME	@

diff --git a/bin/tests/system/dnssec/tests_validation.py b/bin/tests/system/dnssec/tests_validation.py
index 6d9d795c81..e9699c51b8 100644
--- a/bin/tests/system/dnssec/tests_validation.py
+++ b/bin/tests/system/dnssec/tests_validation.py
@@ -711,25 +711,6 @@ def test_cache(ns4):
     for rrset in res2.answer:
         assert rrset.ttl <= 300

-    # RRSIG queries return FORMERR, no matter what the cache
-    # holds for the queried name: prime the cache with a normal query,
-    # then follow it with a query for the corresponding RRSIG
-    msg = isctest.query.create("normalthenrrsig.secure.example", "A")
-    isctest.query.tcp(msg, "10.53.0.4")
-
-    msg = isctest.query.create("normalthenrrsig.secure.example", "RRSIG")
-    res1 = isctest.query.tcp(msg, "10.53.0.3")
-    res2 = isctest.query.tcp(msg, "10.53.0.4")
-    for res in (res1, res2):
-        isctest.check.formerr(res)
-        isctest.check.empty_answer(res)
-        isctest.check.ede(res, 30)
-
-    msg = isctest.query.create("rrsigonly.secure.example", "RRSIG")
-    res1 = isctest.query.tcp(msg, "10.53.0.4")
-    isctest.check.formerr(res1)
-    isctest.check.empty_answer(res1)
-
     # check that a DNSKEY query with no data still gets cached
     msg = isctest.query.create("insecure.example", "DNSKEY")
     res1 = isctest.query.tcp(msg, "10.53.0.4")
diff --git a/bin/tests/system/dnssec_cname_response/ans2/ans.py b/bin/tests/system/dnssec_cname_response/ans2/ans.py
index 99ce5ea834..08fe230b98 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/ans.py
+++ b/bin/tests/system/dnssec_cname_response/ans2/ans.py
@@ -22,7 +22,7 @@ from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.matchers import Domain, Matcher, Qname, Qtype

-# 'example.' answers DNSKEY/NSEC/NSEC3/RRSIG queries with a CNAME (the
+# 'example.' answers DNSKEY/NSEC/NSEC3 queries with a CNAME (the
 # meta-types whose CNAME answer the resolver and validator must cope with).
 EXAMPLE = dns.zone.from_file("example.signed.db", origin="example.", relativize=False)

@@ -37,10 +37,6 @@ SECURE = dns.zone.from_file("secure.signed.db", origin="secure.", relativize=Fal
 STUFFED = dns.zone.from_file("stuffed.signed.zone", origin="stuffed.", relativize=False)


-def a(owner: dns.name.Name, address: str) -> dns.rrset.RRset:
-    return dns.rrset.from_text(owner, 300, dns.rdataclass.IN, dns.rdatatype.A, address)
-
-
 def cname(owner: dns.name.Name, target: str) -> dns.rrset.RRset:
     return dns.rrset.from_text(
         owner, 300, dns.rdataclass.IN, dns.rdatatype.CNAME, target
@@ -61,24 +57,6 @@ def rrset_with_rrsig(
     return rrsets


-class LoneAHandler(ResponseHandler):
-    """
-    Answer any query with a single unrelated A record (no RRSIG and no
-    alias).  An RRSIG query is handled by the resolver as a subset of ANY,
-    and such an answer used to be dropped entirely, leaving the fetch waiting
-    for a validator that was never started.
-    """
-
-    matcher = Domain("lone-a.example.")
-
-    async def get_responses(
-        self, qctx: QueryContext
-    ) -> AsyncGenerator[DnsResponseSend, None]:
-        qctx.prepare_new_response(with_zone_data=False)
-        qctx.response.answer.append(a(qctx.qname, "192.0.2.1"))
-        yield DnsResponseSend(qctx.response)
-
-
 class CnameHandler(ResponseHandler):
     """
     Answer with a CNAME instead of the real records.
@@ -98,7 +76,6 @@ class ExampleMetatypeCnameHandler(CnameHandler):
         dns.rdatatype.DNSKEY,
         dns.rdatatype.NSEC,
         dns.rdatatype.NSEC3,
-        dns.rdatatype.RRSIG,
     )


@@ -199,7 +176,6 @@ def main() -> None:
     server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR, default_aa=True)
     server.install_response_handlers(
         InsecureApexCnameHandler(),
-        LoneAHandler(),
         ExampleMetatypeCnameHandler(),
         SignedZoneHandler(EXAMPLE),
         SecureDsCnameHandler(),
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 7ea3e08add..66a36fb67a 100644
--- a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
+++ b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
@@ -138,7 +138,7 @@ def _check_insecure_cname_chain(res):
     assert answers[dns.rdatatype.A] == EXPECTED_A, res


-@pytest.mark.parametrize("qtype", ["DNSKEY", "NSEC", "NSEC3", "RRSIG"])
+@pytest.mark.parametrize("qtype", ["DNSKEY", "NSEC", "NSEC3"])
 def test_direct_metatype_query_does_not_crash_resolver(qtype):
     """
     A direct recursive client query for a DNSSEC meta-type, answered by a
@@ -158,10 +158,7 @@ def test_direct_metatype_query_does_not_crash_resolver(qtype):
     res = isctest.query.tcp(msg, "10.53.0.3", timeout=8)
     elapsed_time = time.time() - start_time

-    # The resolver must answer promptly. An RRSIG query is handled as a
-    # subset of ANY, and a CNAME answer to it used to be dropped without
-    # caching or validation, leaving the fetch waiting ~12s for a
-    # validator that was never started.
+    # The resolver must answer promptly.
     assert elapsed_time < 5.0, f"{qtype} query took too long: {elapsed_time}s"

     # We do not assert a particular rcode here -- SERVFAIL or a chased
@@ -170,26 +167,6 @@ def test_direct_metatype_query_does_not_crash_resolver(qtype):
     _assert_alive("10.53.0.3")


-def test_rrsig_lone_record_does_not_stall_resolver():
-    """
-    A direct recursive RRSIG query answered with an unrelated record
-    (here a lone A, with no RRSIG and no alias) must not stall the
-    resolver. An RRSIG query is handled as a subset of ANY; every record
-    of the wrong type is filtered out, and when nothing is left the
-    answer used to be accepted as success with no answer bound, leaving
-    the fetch waiting ~12s for a validator that was never started.
-    """
-    msg = isctest.query.create("lone-a.example.", "RRSIG")
-
-    start_time = time.time()
-    res = isctest.query.tcp(msg, "10.53.0.3", timeout=8)
-    elapsed_time = time.time() - start_time
-
-    assert elapsed_time < 5.0, f"RRSIG query took too long: {elapsed_time}s"
-    assert res is not None, "no response to lone-record RRSIG query"
-    _assert_alive("10.53.0.3")
-
-
 def test_cname_for_validator_dnskey_fetch(ns3):
     """
     A malicious authoritative server returning a CNAME for the
diff --git a/bin/tests/system/masterformat/tests.sh b/bin/tests/system/masterformat/tests.sh
index bd4615974d..fbf8977c38 100755
--- a/bin/tests/system/masterformat/tests.sh
+++ b/bin/tests/system/masterformat/tests.sh
@@ -63,7 +63,7 @@ ret=0
 set -- 1 2 3
 for zone in example example-explicit example-compat; do
   for server in "$@"; do
-    for qname in ns mx a aaaa cname dname txt rrsig nsec \
+    for qname in ns mx a aaaa cname dname txt nsec \
       dnskey ds cdnskey cds; do
       qtype="$qname"
       dig_with_opts @10.53.0.${server} -q ${qname}.${zone}. -t ${qtype}
diff --git a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
index ab10c4cc81..5444f823df 100644
--- a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
+++ b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
@@ -17,6 +17,8 @@ by the resolver. ns3 picks the Type-Covered field from the leftmost
 label of QNAME.
 """

+from dns import rdatatype
+
 import pytest

 import isctest
@@ -36,17 +38,30 @@ SIG_TYPES = ["RRSIG"]
 REJECTED_TYPES = META_TYPES + SIG_TYPES


+def any_query(qname):
+    """
+    Standalone RRSIG queries are rejected before any fetch is made, so
+    use an ANY query to make the resolver fetch the lone RRSIG from ans3;
+    with DO set, a cached signature would be returned in the answer.
+    """
+    msg = isctest.query.create(qname, "ANY", dnssec=True, ad=False)
+    return isctest.query.tcp(msg, "10.53.0.2")
+
+
 @pytest.mark.parametrize("covered_type", REJECTED_TYPES)
-def test_rrsig_query_returns_formerr(covered_type):
+def test_rrsig_covers_rejected_type_is_servfail(covered_type):
+    res = any_query(f"{covered_type.lower()}.attacker.test.")
+    isctest.check.servfail(res)
+
+
+def test_rrsig_covers_ordinary_type_is_answered():
     """
-    The resolver rejects RRSIG queries with FORMERR, so the malformed
-    RRSIG can never be pulled into the cache with a direct RRSIG
-    query.
+    The same query is answered when the RRSIG covers an ordinary type,
+    so the SERVFAIL above is caused by the malformed RRSIG.
     """
-    qname = f"{covered_type.lower()}.attacker.test."
-    msg = isctest.query.create(qname, "RRSIG", dnssec=False, ad=False)
-    res = isctest.query.tcp(msg, "10.53.0.2")
-    isctest.check.formerr(res)
+    res = any_query("a.attacker.test.")
+    isctest.check.noerror(res)
+    assert [rrset.rdtype for rrset in res.answer] == [rdatatype.RRSIG], res


 @pytest.mark.parametrize("covered_type", REJECTED_TYPES)
diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh
index bbe9747990..61a284114f 100755
--- a/bin/tests/system/resolver/tests.sh
+++ b/bin/tests/system/resolver/tests.sh
@@ -306,11 +306,23 @@ stop_server ns4
 rm ns4/named.noaa
 start_server --noclean --restart --port ${PORT} ns4 || ret=1

+# Check whether the cache dump in file $2 has an RRSIG covering A at the
+# name $1; the dump prints the owner name only with the first RRset.
+cached_rrsig_a() {
+  awk -v owner="$1" '
+    /^;/ { next }
+    /^[^ \t]/ { current = $1 }
+    current == owner && /[ \t]RRSIG[ \t]+A[ \t]/ { found = 1 }
+    END { exit !found }' "$2"
+}
+
 n=$((n + 1))
 echo_i "check that replacement of additional data by a negative cache no data entry clears the additional RRSIGs ($n)"
 ret=0
 dig_with_opts +tcp mx example.net @10.53.0.7 >dig.ns7.out.${n} || ret=1
 grep "status: NOERROR" dig.ns7.out.${n} >/dev/null || ret=1
+rndc_dumpdb ns7 -cache || ret=1
+cached_rrsig_a mail.example.net. ns7/named_dump.db.test${n} || ret=1
 if [ $ret = 1 ]; then echo_i "mx priming failed"; fi
 $NSUPDATE <<EOF
 server 10.53.0.6 ${PORT}
@@ -325,9 +337,8 @@ grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=2
 if [ $ret = 2 ]; then echo_i "ncache priming failed"; fi
 dig_with_opts +tcp mx example.net @10.53.0.7 >dig.ns7.out.${n} || ret=3
 grep "status: NOERROR" dig.ns7.out.${n} >/dev/null || ret=3
-dig_with_opts +tcp rrsig mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=4
-grep "status: FORMERR" dig.ns7.out.${n} >/dev/null || ret=4
-grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=4
+rndc_dumpdb ns7 -cache || ret=4
+cached_rrsig_a mail.example.net. ns7/named_dump.db.test${n} && ret=4
 dig_with_opts +tcp aaaa mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=5
 grep "status: SERVFAIL" dig.ns7.out.${n} >/dev/null || ret=5
 grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=5
diff --git a/bin/tests/system/resolver/tests_sh_resolver.py b/bin/tests/system/resolver/tests_sh_resolver.py
index 77d3536e75..603a2e23d4 100644
--- a/bin/tests/system/resolver/tests_sh_resolver.py
+++ b/bin/tests/system/resolver/tests_sh_resolver.py
@@ -19,6 +19,7 @@ pytestmark = pytest.mark.extra_artifacts(
         "dig.*.bar.*",
         "dig.*.prime.*",
         "nextpart.out.*",
+        "rndc.out.test*",
         "ans*/query.log",
         "ns1/named.stats*",
         "ns4/tld.db",
@@ -33,6 +34,7 @@ pytestmark = pytest.mark.extra_artifacts(
         "ns6/example.net.db.signed.jnl",
         "ns6/to-be-removed.tld.db",
         "ns6/to-be-removed.tld.db.jnl",
+        "ns7/named_dump.db*",
         "ns7/server.db",
         "ns7/server.db.jnl",
     ]