Commit ab47d0eaa6 for bind

commit ab47d0eaa6916bb439d3c0978793b447f96d014f
Author: Alessio Podda <alessio@isc.org>
Date:   Tue Aug 18 13:47:57 2026 +0200

    Reproduce rollback resign issue

    Add a system test reproducer for rollback breaking the resign time
    of a zone.

diff --git a/bin/tests/system/dnssec/ns3/named.conf.j2 b/bin/tests/system/dnssec/ns3/named.conf.j2
index 4e2b767298..90fb772d44 100644
--- a/bin/tests/system/dnssec/ns3/named.conf.j2
+++ b/bin/tests/system/dnssec/ns3/named.conf.j2
@@ -113,6 +113,7 @@ zone "dynamic.example" {
 	type primary;
 	file "dynamic.example.db.signed";
 	allow-update { any; };
+	max-records-per-type 3;
 };

 zone "insecure.example" {
diff --git a/bin/tests/system/dnssec/tests_signing.py b/bin/tests/system/dnssec/tests_signing.py
index f5c92369f5..bfe16c7d08 100644
--- a/bin/tests/system/dnssec/tests_signing.py
+++ b/bin/tests/system/dnssec/tests_signing.py
@@ -211,6 +211,33 @@ def test_update_signing(default_algorithm):
     assert any("1 0 0 -" in a for a in nsec3)


+def test_update_rollback_preserves_resign_heap(ns3):
+    # Exercise a rollback after an ordinary header has already been removed.
+    # The fourth A record exceeds max-records-per-type and rejects the whole
+    # transaction.  The restored ordinary header must not be inserted into the
+    # DNSSEC re-signing heap with a resign time of zero.
+    up = update.UpdateMessage("dynamic.example.")
+    up.delete("d.dynamic.example.", "A")
+    for address in ("192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4"):
+        up.add("overflow.dynamic.example.", 300, "A", address)
+    res = isctest.query.tcp(up, "10.53.0.3")
+    isctest.check.servfail(res)
+
+    # Confirm that the failed update was rolled back.
+    msg = isctest.query.create("d.dynamic.example.", "A")
+    res = isctest.query.tcp(msg, "10.53.0.3")
+    isctest.check.noerror(res)
+    assert res.answer[0][0].address == "10.0.0.4"
+
+    msg = isctest.query.create("overflow.dynamic.example.", "A")
+    res = isctest.query.tcp(msg, "10.53.0.3")
+    isctest.check.nxdomain(res)
+
+    # A malformed heap entry makes zone.c wrap the next timer into 2106.
+    # The next resign time must still belong to the selected RRset's RRSIG.
+    check_zonestatus_next_resign(ns3, "dynamic.example")
+
+
 def test_cds_signing():
     # check that CDS records are signed using KSK+ZSK by dnssec-signzone
     msg = isctest.query.create("cds.secure.", "CDS")
@@ -390,16 +417,16 @@ def test_rndc_signing_output(ns3):
     assert "No signing records found" in response.out


-def test_zonestatus_signing(ns3):
+def check_zonestatus_next_resign(ns3, zone):
     # check that the correct resigning time is reported in zonestatus.
     # zonestatus reports a name/type and expecting resigning time;
     # we convert the time to seconds since epoch, look up the RRSIG
     # for the name and type, and check that the resigning time is
     # after the inception and before the expiration.

-    response = ns3.rndc("zonestatus secure.example")
+    response = ns3.rndc(f"zonestatus {zone}")

-    # next resign node: secure.example/DNSKEY
+    # next resign node: example/DNSKEY
     nrn = [r for r in response.out.splitlines() if "next resign node" in r][0]
     rdname, rdtype = nrn.split()[3].split("/")

@@ -416,6 +443,10 @@ def test_zonestatus_signing(ns3):
     assert when < sigs[0].expiration


+def test_zonestatus_signing(ns3):
+    check_zonestatus_next_resign(ns3, "secure.example")
+
+
 def test_offline_ksk_signing(ns2, default_algorithm):
     def getfrom(file):
         with open(file, encoding="utf-8") as f: