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