Commit 70ecc82e55 for bind

commit 70ecc82e55d50dac30c433750e5e88156206e2af
Author: Nicki Křížek <nicki@isc.org>
Date:   Mon Oct 19 16:17:31 2026 +0000

    Make the dns64/filter-a reentry test trigger the crash

    The cached NODATA for the AAAA query lived for the SOA's 60-second
    TTL, so it never expired before the A answer arrived and filter-a never
    re-queried AAAA: the test passed with the fix reverted.

    The A answer, delayed by two seconds, also outlasted the resolver's
    retry timeouts.  It was only accepted when a backed-off retry happened
    to cover the delays of the earlier queries queued ahead of it on the
    same TCP connection; on a slow runner none did and the query timed out.

    Give the NODATA a one-second TTL and leave the first two A queries
    unanswered instead.  The test now also checks that filter-a queries
    AAAA again, so that it fails rather than passes without exercising the
    bug if the resolver's retry timing ever changes.

    Assisted-by: Claude:claude-opus-5-5

diff --git a/bin/tests/system/filters/ans6/ans.py b/bin/tests/system/filters/ans6/ans.py
index 5a682f36d6..2c0ea2d290 100644
--- a/bin/tests/system/filters/ans6/ans.py
+++ b/bin/tests/system/filters/ans6/ans.py
@@ -9,24 +9,32 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

+import dns.name
 import dns.rcode
 import dns.rdataclass
 import dns.rdatatype
 import dns.rrset

 from isctest.asyncserver import AsyncDnsServer
-from isctest.asyncserver.handlers import StaticResponseHandler
-from isctest.asyncserver.matchers import Once, Qname, Qtype
+from isctest.asyncserver.handlers import IgnoreAllQueries, StaticResponseHandler
+from isctest.asyncserver.matchers import FirstN, Once, Qname, Qtype

 DNS64_TRIGGER = "nodata.test."


-def rrset(name: str, rdtype: dns.rdatatype.RdataType, *rdata: str) -> dns.rrset.RRset:
-    return dns.rrset.from_text(name, 60, dns.rdataclass.IN, rdtype, *rdata)
+def rrset(
+    owner: str | dns.name.Name,
+    rdtype: dns.rdatatype.RdataType,
+    *rdata: str,
+    ttl: int = 60,
+) -> dns.rrset.RRset:
+    return dns.rrset.from_text(owner, ttl, dns.rdataclass.IN, rdtype, *rdata)


 def soa() -> dns.rrset.RRset:
-    return rrset("test.", dns.rdatatype.SOA, "ns.test. hostmaster.test. 1 2 3 4 5")
+    return rrset(
+        "test.", dns.rdatatype.SOA, "ns.test. hostmaster.test. 1 2 3 4 1", ttl=1
+    )


 def a() -> dns.rrset.RRset:
@@ -37,28 +45,43 @@ def aaaa() -> dns.rrset.RRset:
     return rrset(DNS64_TRIGGER, dns.rdatatype.AAAA, "2001:db8::1")


+class IgnoreFirstTwoAHandler(IgnoreAllQueries):
+    """
+    Leave the resolver's first two A queries unanswered, so that the A answer
+    arrives after the one-second NODATA has expired.  This relies on named
+    waiting at least 1.2 seconds for an answer from a forwarder before it
+    retries: its third A query comes at least 2.4 seconds after the NODATA.
+    The test fails if the NODATA has not expired by then.
+
+    Delaying the A answer instead does not work: named discards an answer
+    that arrives after it has given up on the query.
+    """
+
+    matcher = FirstN(2, Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.A))
+
+
 class NodataOnceHandler(StaticResponseHandler):
     """
     Answer only the first AAAA query, with NODATA, so the DNS64 resolver looks
-    up an A record; the delayed A lookup lets filter-a re-enter and re-query
-    AAAA (answered by AaaaHandler), triggering the bug.
+    up an A record.  If the NODATA expires from the cache before the A answer
+    arrives, filter-a re-enters and re-queries AAAA (answered by AaaaHandler),
+    triggering the bug.
     """

     matcher = Once(Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA))
     authority = [soa()]


+class AHandler(StaticResponseHandler):
+    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.A)
+    answer = [a()]
+
+
 class AaaaHandler(StaticResponseHandler):
     matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)
     answer = [aaaa()]


-class DelayedAHandler(StaticResponseHandler):
-    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.A)
-    answer = [a()]
-    delay = 2.0
-
-
 class FallbackHandler(StaticResponseHandler):
     rcode = dns.rcode.NXDOMAIN
     authority = [soa()]
@@ -67,11 +90,14 @@ class FallbackHandler(StaticResponseHandler):
 def main() -> None:
     server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
     server.install_response_handlers(
+        IgnoreFirstTwoAHandler(),
         NodataOnceHandler(),
+    )
+    server.install_response_handlers(
+        AHandler(),
         AaaaHandler(),
-        DelayedAHandler(),
-        FallbackHandler(),
     )
+    server.install_response_handlers(FallbackHandler())
     server.run()


diff --git a/bin/tests/system/filters/tests_filter_a_dns64.py b/bin/tests/system/filters/tests_filter_a_dns64.py
index ecda5b8f0b..199731b066 100644
--- a/bin/tests/system/filters/tests_filter_a_dns64.py
+++ b/bin/tests/system/filters/tests_filter_a_dns64.py
@@ -9,6 +9,8 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

+from re import compile as Re
+
 import pytest

 from filters.common import ARTIFACTS
@@ -25,9 +27,22 @@ def bootstrap():
     }


-def test_dns64_filter_a_reentry():
+def test_dns64_filter_a_reentry(ns5):
     msg = isctest.query.create("nodata.test.", "aaaa", dnssec=False)
-    res = isctest.query.tcp(msg, "10.53.0.5", attempts=1)
+    with ns5.watch_log_from_here() as watcher:
+        res = isctest.query.tcp(msg, ns5.ip, attempts=1)
+
+        # ans6 holds the A answer back until the NODATA has expired from the
+        # cache, so filter-a has to query AAAA again; without that, the bug
+        # is not exercised at all
+        watcher.wait_for_sequence(
+            [
+                Re(r"fetch: nodata\.test/AAAA$"),
+                Re(r"fetch: nodata\.test/A$"),
+                Re(r"fetch: nodata\.test/AAAA$"),
+            ]
+        )
+
     isctest.check.noerror(res)
     answer = res.get_rrset(res.answer, "nodata.test.", "in", "aaaa")
     assert answer is not None, res