]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Test that an expired RRset does not block negative caching 12423/head
authorOndřej Surý <ondrej@isc.org>
Mon, 20 Jul 2026 05:52:03 +0000 (07:52 +0200)
committerOndřej Surý <ondrej@isc.org>
Mon, 20 Jul 2026 05:52:03 +0000 (07:52 +0200)
Cover the inverse of the CD=1 NXDOMAIN guard: a validated RRset that
has passed its TTL, but has not yet been cleaned from the cache, must
not block the unvalidated negative entry. Before the fix the CD=1
query failed with SERVFAIL until the expired header was reaped.

Assisted-by: Claude:claude-fable-5
bin/tests/system/cdnxdomain/tests_cdnxdomain.py

index 6448dd7b515c9596533346f5e9307f8ddcf291bc..bd53e9841d180298b9a2f17e1b42381ddb597059 100644 (file)
@@ -19,10 +19,15 @@ prime the cache with a validated a.example/A (trust=secure), flip ns2 to a
 version of the zone where a.example no longer exists, and send a CD=1 query
 that makes the resolver cache an unvalidated NXDOMAIN for a.example.  The
 originally cached, more-trusted A record must survive.
+
+The last test covers the inverse: once the validated RRset has passed its
+TTL it must no longer block the negative entry, nor be served to the
+client in its place.
 """
 
 import os
 import shutil
+import time
 
 import dns.zone
 
@@ -31,6 +36,7 @@ import isctest
 RESOLVER = "10.53.0.1"
 AUTH = "10.53.0.2"
 A_RDATA = "10.53.0.99"
+SHORT_TTL = 3
 
 
 def bootstrap():
@@ -57,7 +63,7 @@ ns2 A 10.53.0.2
         f"-P -x -O full -o {name} -f {name}-empty.db.signed {name}-empty.db", cwd="ns2"
     )
 
-    zonetext += "a A 10.53.0.99"
+    zonetext += f"a A 10.53.0.99\nshort {SHORT_TTL} A 10.53.0.99\n"
     zone = dns.zone.from_text(zonetext, origin=name)
     zone.to_file(f"ns2/{name}-full.db", sorted=True)
     signer(
@@ -151,3 +157,30 @@ def test_cd1_nxdomain_uncached_type_answer(ns1, ns2, system_test_dir):
     isctest.check.nxdomain(res)
     isctest.check.adflag(res)
     isctest.check.empty_answer(res)
+
+
+def test_cd1_nxdomain_expired_secure_does_not_block(ns1, ns2, system_test_dir):
+    # Prime the resolver with the short-TTL validated short.example/A.
+    _serve(ns2, system_test_dir, "full")
+    ns1.rndc("flush")
+    res = isctest.query.tcp(isctest.query.create("short.example", "A"), RESOLVER)
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+    assert A_RDATA in str(res.answer), res.answer
+
+    # Flip the zone to the version without the name and wait out the TTL.
+    # Expired headers are only cleaned lazily, so the secure A record is
+    # still sitting in the cache when the negative entry arrives.
+    _serve(ns2, system_test_dir, "empty")
+    time.sleep(SHORT_TTL + 2)
+    direct = isctest.query.create("short.example", "A", dnssec=False)
+    isctest.check.nxdomain(isctest.query.tcp(direct, AUTH))
+
+    # The expired secure A record must neither block the unvalidated
+    # NXDOMAIN from being cached nor be handed back to the client in its
+    # place.
+    res = isctest.query.tcp(
+        isctest.query.create("short.example", "A", cd=True), RESOLVER
+    )
+    isctest.check.nxdomain(res)
+    isctest.check.empty_answer(res)