Open
fix(cve): 修复 CVE-2026-75029 在 bind 中的漏洞 #344
fix(cve): 修复 CVE-2026-75029 在 bind 中的漏洞 #344
Open
infra_teamAdd on Sep 17
Total: 38 files changed+24-8985
@@ -1,69 +0,0 @@
1-From e3b23edcef81abe6b11e2fc5b6c0f656e0ce250a Mon Sep 17 00:00:00 2001
2-From: Evan Hunt <each@isc.org>
3-Date: Thu, 21 May 2026 14:41:55 -0700
4-Subject: [PATCH] Check NSEC3 signer matches the owning zone
5- 
6-When validating NSEC3 records, reject any signature whose signer field
7-does not match the zone owning the NSEC3.
8- 
9-This ensures that a child zone cannot impersonate its parent and forge
10-NXDOMAIN responses for sibling domains.
11- 
12-Fixes: isc-projects/bind9#5874
13-(cherry picked from commit 6e5066bb1f0f12d090e8707adb7d6ccf74f8012b)
14- 
15-Conflict:no
16-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e3b23edcef81abe6b11e2fc5b6c0f656e0ce250a
17----
18- lib/dns/dnssec.c | 19 +++++++++++++++++--
19- lib/isc/result.c | 2 +-
20- 2 files changed, 18 insertions(+), 3 deletions(-)
21- 
22-diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
23-index 4b5ddc2184..712a7880fc 100644
24---- a/lib/dns/dnssec.c
25-+++ b/lib/dns/dnssec.c
26-@@ -423,10 +423,25 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
27- }
28-
29- /*
30-- * NS, SOA and DNSKEY records are signed by their owner.
31-- * DS records are signed by the parent.
32-+ * NS, SOA and DNSKEY records are signed by their owners.
33-+ * NSEC3 records are signed by the apex, exactly one level up
34-+ * from their owner names.
35-+ * DS records are signed by the parent zone.
36- */
37- switch (set->type) {
38-+ case dns_rdatatype_nsec3: {
39-+ dns_name_t apex = DNS_NAME_INITEMPTY;
40-+ labels = dns_name_countlabels(name);
41-+ if (labels <= 1) {
42-+ inc_stat(dns_dnssecstats_fail);
43-+ return DNS_R_INVALIDNSEC3;
44-+ }
45-+ dns_name_split(name, labels - 1, NULL, &apex);
46-+ if (!dns_name_equal(&apex, &sig.signer)) {
47-+ inc_stat(dns_dnssecstats_fail);
48-+ return DNS_R_SIGINVALID;
49-+ }
50-+ } break;
51- case dns_rdatatype_ns:
52- case dns_rdatatype_soa:
53- case dns_rdatatype_dnskey:
54-diff --git a/lib/isc/result.c b/lib/isc/result.c
55-index 0c608deecd..86fdce5ef4 100644
56---- a/lib/isc/result.c
57-+++ b/lib/isc/result.c
58-@@ -200,7 +200,7 @@ static const char *description[ISC_R_NRESULTS] = {
59- [DNS_R_COVERINGNSEC] = "covering NSEC record returned",
60- [DNS_R_MXISADDRESS] = "MX is an address",
61- [DNS_R_DUPLICATE] = "duplicate query",
62-- [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name (wildcard)",
63-+ [DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name",
64- [DNS_R_NOTPRIMARY] = "not primary",
65- [DNS_R_BROKENCHAIN] = "broken trust chain",
66- [DNS_R_EXPIRED] = "expired",
67---
68-2.43.0
69- 
@@ -1,495 +0,0 @@
1-From 152bffd0d7ab03fdb096432a9274a2919be75fe0 Mon Sep 17 00:00:00 2001
2-From: Alessio Podda <alessio@isc.org>
3-Date: Wed, 3 Jun 2026 10:42:43 +0200
4-Subject: [PATCH] Reproducer for #5874 NSEC3 impersonation
5- 
6-LLM generated.
7- 
8-(cherry picked from commit f3e2eb333be3ac636f745aa13cfb8d9ee8af87d8)
9- 
10-Conflict:no
11-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/152bffd0d7ab03fdb096432a9274a2919be75fe0
12----
13- .../repro_5874_nsec3_parent/ans1/ans.py | 18 ++
14- .../repro_5874_nsec3_parent/ns2/named.conf.j2 | 35 +++
15- .../system/repro_5874_nsec3_parent/server.py | 249 ++++++++++++++++++
16- .../tests_repro_5874_nsec3_parent.py | 144 ++++++++++
17- 4 files changed, 446 insertions(+)
18- create mode 100644 bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
19- create mode 100644 bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
20- create mode 100644 bin/tests/system/repro_5874_nsec3_parent/server.py
21- create mode 100644 bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
22- 
23-diff --git a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
24-new file mode 100644
25-index 0000000000..614f26a014
26---- /dev/null
27-+++ b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
28-@@ -0,0 +1,18 @@
29-+#!/usr/bin/python3
30-+
31-+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
32-+#
33-+# SPDX-License-Identifier: MPL-2.0
34-+#
35-+# This Source Code Form is subject to the terms of the Mozilla Public
36-+# License, v. 2.0. If a copy of the MPL was not distributed with this
37-+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
38-+#
39-+# See the COPYRIGHT file distributed with this work for additional
40-+# information regarding copyright ownership.
41-+
42-+from server import main
43-+
44-+
45-+if __name__ == "__main__":
46-+ main()
47-diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
48-new file mode 100644
49-index 0000000000..9358866373
50---- /dev/null
51-+++ b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
52-@@ -0,0 +1,35 @@
53-+// validating resolver
54-+
55-+options {
56-+ query-source address 10.53.0.2;
57-+ notify-source 10.53.0.2;
58-+ transfer-source 10.53.0.2;
59-+ port @PORT@;
60-+ pid-file "named.pid";
61-+ listen-on { 10.53.0.2; };
62-+ listen-on-v6 { none; };
63-+ recursion yes;
64-+ dnssec-validation yes;
65-+ trust-anchor-telemetry no;
66-+ resolver-query-timeout 5000;
67-+};
68-+
69-+controls {
70-+ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
71-+};
72-+
73-+include "../../_common/rndc.key";
74-+
75-+zone "." {
76-+ type hint;
77-+ file "../../_common/root.hint";
78-+};
79-+
80-+zone "tld.test" {
81-+ type static-stub;
82-+ server-addresses { 10.53.0.1; };
83-+};
84-+
85-+trust-anchors {
86-+ tld.test. static-key 257 3 13 "@TLD_DNSKEY@";
87-+};
88-diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/repro_5874_nsec3_parent/server.py
89-new file mode 100644
90-index 0000000000..c6856bb228
91---- /dev/null
92-+++ b/bin/tests/system/repro_5874_nsec3_parent/server.py
93-@@ -0,0 +1,249 @@
94-+#!/usr/bin/python3
95-+
96-+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
97-+#
98-+# SPDX-License-Identifier: MPL-2.0
99-+#
100-+# This Source Code Form is subject to the terms of the Mozilla Public
101-+# License, v. 2.0. If a copy of the MPL was not distributed with this
102-+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
103-+#
104-+# See the COPYRIGHT file distributed with this work for additional
105-+# information regarding copyright ownership.
106-+
107-+from collections.abc import AsyncGenerator
108-+from dataclasses import dataclass
109-+from pathlib import Path
110-+
111-+import json
112-+
113-+from cryptography.hazmat.primitives import serialization
114-+
115-+import dns.dnssec
116-+import dns.flags
117-+import dns.message
118-+import dns.name
119-+import dns.rdata
120-+import dns.rdataclass
121-+import dns.rcode
122-+import dns.rdatatype
123-+import dns.rrset
124-+
125-+from isctest.asyncserver import (
126-+ AsyncDnsServer,
127-+ DnsResponseSend,
128-+ QueryContext,
129-+ ResponseHandler,
130-+)
131-+
132-+TTL = 300
133-+TLD = "tld.test."
134-+APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
135-+ATTACKER = f"{APEX_HASH.lower()}.{TLD}"
136-+VICTIM = f"victim.{TLD}"
137-+AUTH_IP = "10.53.0.1"
138-+
139-+
140-+@dataclass(frozen=True)
141-+class Key:
142-+ zone: dns.name.Name
143-+ private_key: object
144-+ dnskey: dns.rdata.Rdata
145-+ ds: dns.rdata.Rdata
146-+
147-+
148-+def name(text: str) -> dns.name.Name:
149-+ return dns.name.from_text(text)
150-+
151-+
152-+def load_keys() -> dict[str, Key]:
153-+ path = Path(__file__).resolve().parent / "keys.json"
154-+ with path.open(encoding="utf-8") as keys_file:
155-+ raw_keys = json.load(keys_file)
156-+
157-+ keys = {}
158-+ for zone, raw_key in raw_keys.items():
159-+ private_key = serialization.load_pem_private_key(
160-+ raw_key["private_pem"].encode("ascii"),
161-+ password=None,
162-+ )
163-+ dnskey = dns.rdata.from_text(
164-+ dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"]
165-+ )
166-+ ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"])
167-+ keys[zone] = Key(name(zone), private_key, dnskey, ds)
168-+
169-+ return keys
170-+
171-+
172-+def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset:
173-+ return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas)
174-+
175-+
176-+def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset:
177-+ return dns.rrset.from_rdata(name(owner), TTL, rdata)
178-+
179-+
180-+def rrsig_rrset(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset:
181-+ rrsig = dns.dnssec.sign(
182-+ covered,
183-+ signer.private_key,
184-+ signer.zone,
185-+ signer.dnskey,
186-+ lifetime=86400,
187-+ verify=True,
188-+ )
189-+ return dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)
190-+
191-+
192-+def add_signed(
193-+ section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key
194-+) -> None:
195-+ section.append(covered)
196-+ section.append(rrsig_rrset(covered, signer))
197-+
198-+
199-+def dnskey_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset:
200-+ return rrset_from_rdata(zone, zone_key.dnskey)
201-+
202-+
203-+def ds_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset:
204-+ return rrset_from_rdata(zone, zone_key.ds)
205-+
206-+
207-+def soa_rrset(zone: str) -> dns.rrset.RRset:
208-+ return rrset(
209-+ zone,
210-+ dns.rdatatype.SOA,
211-+ f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300",
212-+ )
213-+
214-+
215-+def ns_rrset(zone: str, ns_target: str) -> dns.rrset.RRset:
216-+ return rrset(zone, dns.rdatatype.NS, ns_target)
217-+
218-+
219-+def glue_rrset(ns_target: str, address: str) -> dns.rrset.RRset:
220-+ return rrset(ns_target, dns.rdatatype.A, address)
221-+
222-+
223-+def answer_dnskey(response: dns.message.Message, zone: str, zone_key: Key) -> None:
224-+ add_signed(response.answer, dnskey_rrset(zone, zone_key), zone_key)
225-+
226-+
227-+def answer_soa(response: dns.message.Message, zone: str, zone_key: Key) -> None:
228-+ add_signed(response.answer, soa_rrset(zone), zone_key)
229-+
230-+
231-+def answer_ns(
232-+ response: dns.message.Message, zone: str, ns_target: str, zone_key: Key
233-+) -> None:
234-+ add_signed(response.answer, ns_rrset(zone, ns_target), zone_key)
235-+
236-+
237-+def answer_ds(
238-+ response: dns.message.Message, zone: str, child_key: Key, parent_key: Key
239-+) -> None:
240-+ add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
241-+
242-+
243-+def child_nsec3_rrset() -> dns.rrset.RRset:
244-+ rdata = dns.rdata.from_text(
245-+ dns.rdataclass.IN,
246-+ dns.rdatatype.NSEC3,
247-+ f"1 0 0 - {APEX_HASH} NS SOA RRSIG DNSKEY NSEC3PARAM",
248-+ )
249-+ return dns.rrset.from_rdata(name(f"{APEX_HASH}.{TLD}"), TTL, rdata)
250-+
251-+
252-+def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None:
253-+ response.set_rcode(dns.rcode.NXDOMAIN)
254-+
255-+ add_signed(response.authority, soa_rrset(TLD), keys[TLD])
256-+
257-+ # The owner name derives zone "tld.test.", but the RRSIG signer is the
258-+ # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
259-+ add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER])
260-+
261-+
262-+class SignedResponseHandler(ResponseHandler):
263-+ def __init__(self, keys: dict[str, Key]) -> None:
264-+ self.keys = keys
265-+
266-+ async def get_responses(
267-+ self, qctx: QueryContext
268-+ ) -> AsyncGenerator[DnsResponseSend, None]:
269-+ qctx.prepare_new_response(with_zone_data=False)
270-+ qctx.response.flags |= dns.flags.AA
271-+ qctx.response.set_rcode(dns.rcode.NOERROR)
272-+ self.respond(qctx)
273-+ yield DnsResponseSend(qctx.response, authoritative=True)
274-+
275-+ def respond(self, qctx: QueryContext) -> None:
276-+ raise NotImplementedError
277-+
278-+
279-+class VictimForgedNxdomainHandler(SignedResponseHandler):
280-+ def match(self, qctx: QueryContext) -> bool:
281-+ return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A
282-+
283-+ def respond(self, qctx: QueryContext) -> None:
284-+ forged_nxdomain(qctx.response, self.keys)
285-+
286-+
287-+class ChildDsHandler(SignedResponseHandler):
288-+ def match(self, qctx: QueryContext) -> bool:
289-+ return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS
290-+
291-+ def respond(self, qctx: QueryContext) -> None:
292-+ answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD])
293-+
294-+
295-+class AttackerZoneHandler(SignedResponseHandler):
296-+ def match(self, qctx: QueryContext) -> bool:
297-+ return qctx.qname.is_subdomain(name(ATTACKER))
298-+
299-+ def respond(self, qctx: QueryContext) -> None:
300-+ if qctx.qname == name(ATTACKER):
301-+ if qctx.qtype == dns.rdatatype.DNSKEY:
302-+ answer_dnskey(qctx.response, ATTACKER, self.keys[ATTACKER])
303-+ elif qctx.qtype == dns.rdatatype.SOA:
304-+ answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER])
305-+ else:
306-+ answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER])
307-+ qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP))
308-+ return
309-+
310-+ qctx.response.set_rcode(dns.rcode.NXDOMAIN)
311-+ add_signed(qctx.response.authority, soa_rrset(ATTACKER), self.keys[ATTACKER])
312-+
313-+
314-+class TldZoneHandler(SignedResponseHandler):
315-+ def match(self, qctx: QueryContext) -> bool:
316-+ return qctx.qname.is_subdomain(name(TLD))
317-+
318-+ def respond(self, qctx: QueryContext) -> None:
319-+ if qctx.qname == name(TLD):
320-+ if qctx.qtype == dns.rdatatype.DNSKEY:
321-+ answer_dnskey(qctx.response, TLD, self.keys[TLD])
322-+ elif qctx.qtype == dns.rdatatype.SOA:
323-+ answer_soa(qctx.response, TLD, self.keys[TLD])
324-+ else:
325-+ answer_ns(qctx.response, TLD, "ns.tld.test.", self.keys[TLD])
326-+ qctx.response.additional.append(glue_rrset("ns.tld.test.", AUTH_IP))
327-+ return
328-+
329-+ qctx.response.set_rcode(dns.rcode.NXDOMAIN)
330-+ add_signed(qctx.response.authority, soa_rrset(TLD), self.keys[TLD])
331-+
332-+
333-+def main() -> None:
334-+ keys = load_keys()
335-+ server = AsyncDnsServer(default_aa=True)
336-+ server.install_response_handlers(
337-+ VictimForgedNxdomainHandler(keys),
338-+ ChildDsHandler(keys),
339-+ AttackerZoneHandler(keys),
340-+ TldZoneHandler(keys),
341-+ )
342-+ server.run()
343-diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
344-new file mode 100644
345-index 0000000000..5fbeff5bef
346---- /dev/null
347-+++ b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
348-@@ -0,0 +1,144 @@
349-+#!/usr/bin/python3
350-+
351-+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
352-+#
353-+# SPDX-License-Identifier: MPL-2.0
354-+#
355-+# This Source Code Form is subject to the terms of the Mozilla Public
356-+# License, v. 2.0. If a copy of the MPL was not distributed with this
357-+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
358-+#
359-+# See the COPYRIGHT file distributed with this work for additional
360-+# information regarding copyright ownership.
361-+
362-+from pathlib import Path
363-+
364-+import json
365-+
366-+from cryptography.hazmat.primitives import serialization
367-+from cryptography.hazmat.primitives.asymmetric import ec
368-+
369-+import dns.dnssec
370-+import dns.flags
371-+import dns.name
372-+import dns.rdataclass
373-+import dns.rdatatype
374-+import pytest
375-+
376-+import isctest
377-+
378-+APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
379-+ATTACKER = f"{APEX_HASH.lower()}.tld.test."
380-+VICTIM = "victim.tld.test."
381-+AUTH = "10.53.0.1"
382-+RESOLVER = "10.53.0.2"
383-+
384-+pytestmark = pytest.mark.extra_artifacts(
385-+ [
386-+ "ans*/ans.run",
387-+ "keys.json",
388-+ ]
389-+)
390-+
391-+
392-+def _make_key(zone):
393-+ private_key = ec.generate_private_key(ec.SECP256R1())
394-+ dnskey = dns.dnssec.make_dnskey(
395-+ private_key.public_key(),
396-+ algorithm="ECDSAP256SHA256",
397-+ flags=257,
398-+ )
399-+ ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256")
400-+ private_pem = private_key.private_bytes(
401-+ encoding=serialization.Encoding.PEM,
402-+ format=serialization.PrivateFormat.PKCS8,
403-+ encryption_algorithm=serialization.NoEncryption(),
404-+ ).decode("ascii")
405-+ return {
406-+ "private_pem": private_pem,
407-+ "dnskey": dnskey.to_text(),
408-+ "ds": ds.to_text(),
409-+ }
410-+
411-+
412-+def bootstrap():
413-+ zones = ["tld.test.", ATTACKER]
414-+ keys = {zone: _make_key(zone) for zone in zones}
415-+
416-+ Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
417-+
418-+ tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:])
419-+ return {"TLD_DNSKEY": tld_dnskey}
420-+
421-+
422-+def _check_direct_dnskey_response(zone):
423-+ query = isctest.query.create(zone, "DNSKEY")
424-+ response = isctest.query.tcp(query, AUTH)
425-+
426-+ isctest.check.noerror(response)
427-+ assert response.flags & dns.flags.AA
428-+ assert (
429-+ response.get_rrset(
430-+ response.answer,
431-+ dns.name.from_text(zone),
432-+ dns.rdataclass.IN,
433-+ dns.rdatatype.DNSKEY,
434-+ )
435-+ is not None
436-+ ), response
437-+
438-+
439-+def _check_direct_ds_response(zone):
440-+ query = isctest.query.create(zone, "DS")
441-+ response = isctest.query.tcp(query, AUTH)
442-+
443-+ isctest.check.noerror(response)
444-+ assert response.flags & dns.flags.AA
445-+ assert (
446-+ response.get_rrset(
447-+ response.answer,
448-+ dns.name.from_text(zone),
449-+ dns.rdataclass.IN,
450-+ dns.rdatatype.DS,
451-+ )
452-+ is not None
453-+ ), response
454-+
455-+
456-+def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
457-+ _check_direct_dnskey_response("tld.test.")
458-+ _check_direct_dnskey_response(ATTACKER)
459-+ _check_direct_ds_response(ATTACKER)
460-+
461-+ query = isctest.query.create(VICTIM, "A")
462-+ response = isctest.query.tcp(query, AUTH)
463-+
464-+ isctest.check.nxdomain(response)
465-+ assert response.flags & dns.flags.AA
466-+
467-+ nsec3_owner = dns.name.from_text(f"{APEX_HASH}.tld.test.")
468-+ nsec3 = response.get_rrset(
469-+ response.authority,
470-+ nsec3_owner,
471-+ dns.rdataclass.IN,
472-+ dns.rdatatype.NSEC3,
473-+ )
474-+ rrsig = response.get_rrset(
475-+ response.authority,
476-+ nsec3_owner,
477-+ dns.rdataclass.IN,
478-+ dns.rdatatype.RRSIG,
479-+ covers=dns.rdatatype.NSEC3,
480-+ )
481-+
482-+ assert nsec3 is not None, response
483-+ assert rrsig is not None, response
484-+ assert rrsig[0].signer == dns.name.from_text(ATTACKER)
485-+
486-+
487-+def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof():
488-+ query = isctest.query.create(VICTIM, "A")
489-+ response = isctest.query.tcp(query, RESOLVER)
490-+
491-+ isctest.check.servfail(response)
492-+ isctest.check.noadflag(response)
493---
494-2.43.0
495- 
@@ -1,309 +0,0 @@
1-From f4167e8b76e614e52245b82caae1a97da6800183 Mon Sep 17 00:00:00 2001
2-From: Matthijs Mekking <matthijs@isc.org>
3-Date: Thu, 4 Jun 2026 16:12:50 +0200
4-Subject: [PATCH] Update reproducer #5874
5- 
6-Update the llm generated reproducer:
7-- Move server.py into ans1/ans.py
8-- Remove unnecessary named.conf configuration options
9-- Add comments describing the steps (copied from GL issue)
10-- Rename system test
11- 
12-(cherry picked from commit c1321fef165a2ef8c2bff971901c58941e8e694c)
13- 
14-Conflict:no
15-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/f4167e8b76e614e52245b82caae1a97da6800183
16----
17- .../ans1/ans.py} | 81 +++++++++++++------
18- .../ns2/named.conf.j2 | 2 -
19- .../tests_nsec3_impersonation.py} | 38 +++++----
20- .../repro_5874_nsec3_parent/ans1/ans.py | 18 -----
21- 4 files changed, 79 insertions(+), 60 deletions(-)
22- rename bin/tests/system/{repro_5874_nsec3_parent/server.py => nsec3_impersonation/ans1/ans.py} (83%)
23- rename bin/tests/system/{repro_5874_nsec3_parent => nsec3_impersonation}/ns2/named.conf.j2 (90%)
24- rename bin/tests/system/{repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py => nsec3_impersonation/tests_nsec3_impersonation.py} (84%)
25- delete mode 100644 bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
26- 
27-diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/nsec3_impersonation/ans1/ans.py
28-similarity index 83%
29-rename from bin/tests/system/repro_5874_nsec3_parent/server.py
30-rename to bin/tests/system/nsec3_impersonation/ans1/ans.py
31-index c6856bb228..177e79c195 100644
32---- a/bin/tests/system/repro_5874_nsec3_parent/server.py
33-+++ b/bin/tests/system/nsec3_impersonation/ans1/ans.py
34-@@ -23,9 +23,9 @@ import dns.dnssec
35- import dns.flags
36- import dns.message
37- import dns.name
38-+import dns.rcode
39- import dns.rdata
40- import dns.rdataclass
41--import dns.rcode
42- import dns.rdatatype
43- import dns.rrset
44-
45-@@ -141,10 +141,21 @@ def answer_ns(
46- add_signed(response.answer, ns_rrset(zone, ns_target), zone_key)
47-
48-
49--def answer_ds(
50-- response: dns.message.Message, zone: str, child_key: Key, parent_key: Key
51--) -> None:
52-- add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
53-+class SignedResponseHandler(ResponseHandler):
54-+ def __init__(self, keys: dict[str, Key]) -> None:
55-+ self.keys = keys
56-+
57-+ async def get_responses(
58-+ self, qctx: QueryContext
59-+ ) -> AsyncGenerator[DnsResponseSend, None]:
60-+ qctx.prepare_new_response(with_zone_data=False)
61-+ qctx.response.flags |= dns.flags.AA
62-+ qctx.response.set_rcode(dns.rcode.NOERROR)
63-+ self.respond(qctx)
64-+ yield DnsResponseSend(qctx.response, authoritative=True)
65-+
66-+ def respond(self, qctx: QueryContext) -> None:
67-+ raise NotImplementedError
68-
69-
70- def child_nsec3_rrset() -> dns.rrset.RRset:
71-@@ -162,28 +173,15 @@ def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None
72- add_signed(response.authority, soa_rrset(TLD), keys[TLD])
73-
74- # The owner name derives zone "tld.test.", but the RRSIG signer is the
75-- # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
76-+ # malicious child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
77- add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER])
78-
79-
80--class SignedResponseHandler(ResponseHandler):
81-- def __init__(self, keys: dict[str, Key]) -> None:
82-- self.keys = keys
83--
84-- async def get_responses(
85-- self, qctx: QueryContext
86-- ) -> AsyncGenerator[DnsResponseSend, None]:
87-- qctx.prepare_new_response(with_zone_data=False)
88-- qctx.response.flags |= dns.flags.AA
89-- qctx.response.set_rcode(dns.rcode.NOERROR)
90-- self.respond(qctx)
91-- yield DnsResponseSend(qctx.response, authoritative=True)
92--
93-- def respond(self, qctx: QueryContext) -> None:
94-- raise NotImplementedError
95--
96--
97- class VictimForgedNxdomainHandler(SignedResponseHandler):
98-+ """
99-+ This serves the forged response for the victim's domain.
100-+ """
101-+
102- def match(self, qctx: QueryContext) -> bool:
103- return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A
104-
105-@@ -192,14 +190,31 @@ class VictimForgedNxdomainHandler(SignedResponseHandler):
106-
107-
108- class ChildDsHandler(SignedResponseHandler):
109-+ """
110-+ This will spoof the response for the malicious zone when qtype is DS.
111-+ It is actually a validly signed DS response.
112-+ """
113-+
114- def match(self, qctx: QueryContext) -> bool:
115- return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS
116-
117- def respond(self, qctx: QueryContext) -> None:
118-- answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD])
119-+ response = qctx.response
120-+ zone = ATTACKER
121-+ child_key = self.keys[ATTACKER]
122-+ parent_key = self.keys[TLD]
123-+
124-+ add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
125-
126-
127- class AttackerZoneHandler(SignedResponseHandler):
128-+ """
129-+ Acts as the malicious authoritative name server. The zone being served
130-+ is the hashed label of the parent zone (tld.test). This will respond
131-+ for all queries qtype SOA, DNSKEY, NS at the apex. Any names below
132-+ the apex are answered with an NXDOMAIN with no NSEC or NSEC3 present.
133-+ """
134-+
135- def match(self, qctx: QueryContext) -> bool:
136- return qctx.qname.is_subdomain(name(ATTACKER))
137-
138-@@ -210,7 +225,9 @@ class AttackerZoneHandler(SignedResponseHandler):
139- elif qctx.qtype == dns.rdatatype.SOA:
140- answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER])
141- else:
142-- answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER])
143-+ answer_ns(
144-+ qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER]
145-+ )
146- qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP))
147- return
148-
149-@@ -219,6 +236,16 @@ class AttackerZoneHandler(SignedResponseHandler):
150-
151-
152- class TldZoneHandler(SignedResponseHandler):
153-+ """
154-+ Acts as the TLD who is being used in the attack, but is not a standard
155-+ name server. It only responds with validly signed records for DNSKEY, SOA
156-+ and NS on the apex. Any names below the apex are answered with an NXDOMAIN
157-+ with no NSEC or NSEC3 present.
158-+
159-+ If we turn this into a regular name server than the attack won't work.
160-+ The attack assumes that the adversary can inject these responses on-path.
161-+ """
162-+
163- def match(self, qctx: QueryContext) -> bool:
164- return qctx.qname.is_subdomain(name(TLD))
165-
166-@@ -247,3 +274,7 @@ def main() -> None:
167- TldZoneHandler(keys),
168- )
169- server.run()
170-+
171-+
172-+if __name__ == "__main__":
173-+ main()
174-diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
175-similarity index 90%
176-rename from bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
177-rename to bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
178-index 9358866373..2c9b0bba9e 100644
179---- a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
180-+++ b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
181-@@ -10,8 +10,6 @@ options {
182- listen-on-v6 { none; };
183- recursion yes;
184- dnssec-validation yes;
185-- trust-anchor-telemetry no;
186-- resolver-query-timeout 5000;
187- };
188-
189- controls {
190-diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
191-similarity index 84%
192-rename from bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
193-rename to bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
194-index 5fbeff5bef..bd9bd275b6 100644
195---- a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
196-+++ b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
197-@@ -26,6 +26,7 @@ import dns.rdatatype
198- import pytest
199-
200- import isctest
201-+import isctest.mark
202-
203- APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
204- ATTACKER = f"{APEX_HASH.lower()}.tld.test."
205-@@ -33,12 +34,15 @@ VICTIM = "victim.tld.test."
206- AUTH = "10.53.0.1"
207- RESOLVER = "10.53.0.2"
208-
209--pytestmark = pytest.mark.extra_artifacts(
210-- [
211-- "ans*/ans.run",
212-- "keys.json",
213-- ]
214--)
215-+pytestmark = [
216-+ isctest.mark.with_ecdsa_deterministic,
217-+ pytest.mark.extra_artifacts(
218-+ [
219-+ "ans*/ans.run",
220-+ "ans*/keys.json",
221-+ ]
222-+ ),
223-+]
224-
225-
226- def _make_key(zone):
227-@@ -65,13 +69,13 @@ def bootstrap():
228- zones = ["tld.test.", ATTACKER]
229- keys = {zone: _make_key(zone) for zone in zones}
230-
231-- Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
232-+ Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
233-
234- tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:])
235- return {"TLD_DNSKEY": tld_dnskey}
236-
237-
238--def _check_direct_dnskey_response(zone):
239-+def check_dnskey_response(zone):
240- query = isctest.query.create(zone, "DNSKEY")
241- response = isctest.query.tcp(query, AUTH)
242-
243-@@ -88,7 +92,7 @@ def _check_direct_dnskey_response(zone):
244- ), response
245-
246-
247--def _check_direct_ds_response(zone):
248-+def check_ds_response(zone):
249- query = isctest.query.create(zone, "DS")
250- response = isctest.query.tcp(query, AUTH)
251-
252-@@ -105,10 +109,10 @@ def _check_direct_ds_response(zone):
253- ), response
254-
255-
256--def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
257-- _check_direct_dnskey_response("tld.test.")
258-- _check_direct_dnskey_response(ATTACKER)
259-- _check_direct_ds_response(ATTACKER)
260-+def test_attack_responses():
261-+ check_dnskey_response("tld.test.")
262-+ check_dnskey_response(ATTACKER)
263-+ check_ds_response(ATTACKER)
264-
265- query = isctest.query.create(VICTIM, "A")
266- response = isctest.query.tcp(query, AUTH)
267-@@ -136,9 +140,13 @@ def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
268- assert rrsig[0].signer == dns.name.from_text(ATTACKER)
269-
270-
271--def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof():
272-+def test_nsec3_impersonation():
273-+ """
274-+ Reproducer for #5874:
275-+ F-006 DNSSEC Validation Bypass NSEC3 Apex Hash Label Parent Impersonation
276-+ """
277- query = isctest.query.create(VICTIM, "A")
278- response = isctest.query.tcp(query, RESOLVER)
279-
280-- isctest.check.servfail(response)
281- isctest.check.noadflag(response)
282-+ isctest.check.servfail(response)
283-diff --git a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
284-deleted file mode 100644
285-index 614f26a014..0000000000
286---- a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
287-+++ /dev/null
288-@@ -1,18 +0,0 @@
289--#!/usr/bin/python3
290--
291--# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
292--#
293--# SPDX-License-Identifier: MPL-2.0
294--#
295--# This Source Code Form is subject to the terms of the Mozilla Public
296--# License, v. 2.0. If a copy of the MPL was not distributed with this
297--# file, you can obtain one at https://mozilla.org/MPL/2.0/.
298--#
299--# See the COPYRIGHT file distributed with this work for additional
300--# information regarding copyright ownership.
301--
302--from server import main
303--
304--
305--if __name__ == "__main__":
306-- main()
307---
308-2.43.0
309- 
@@ -1,65 +0,0 @@
1-From fc3ca9e494c277350ca0242721425cace79df82d Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 19 May 2026 10:44:04 +1000
4-Subject: [PATCH] Check that dns_name_fromwire honours the active region
5- 
6-When reading DNS records from the wire the active region of the
7-source buffer is set to the end of the current record. dns_name_fromwire
8-should fail if it attempts to read past this setting.
9- 
10-(cherry picked from commit 3ed821d68b15fe4e6288e3054397d6bce7e65968)
11- 
12-Conflict:no
13-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/fc3ca9e494c277350ca0242721425cace79df82d
14----
15- tests/dns/name_test.c | 27 +++++++++++++++++++++++++++
16- 1 file changed, 27 insertions(+)
17- 
18-diff --git a/tests/dns/name_test.c b/tests/dns/name_test.c
19-index 172b977708..85ffb2d6e5 100644
20---- a/tests/dns/name_test.c
21-+++ b/tests/dns/name_test.c
22-@@ -518,6 +518,32 @@ ISC_RUN_TEST_IMPL(fromregion) {
23- assert_false(dns_name_isabsolute(&name));
24- }
25-
26-+ISC_RUN_TEST_IMPL(fromwire) {
27-+ dns_fixedname_t fixed;
28-+ dns_name_t *name = dns_fixedname_initname(&fixed);
29-+ isc_buffer_t b;
30-+ unsigned char source[] = { 0x03, 'o', 'n', 'e', 0x00, 0x03,
31-+ 't', 'w', 'o', 0x00, 0x05, 't',
32-+ 'h', 'r', 'e', 'e', 0x00 };
33-+ isc_result_t result;
34-+
35-+ isc_buffer_init(&b, source, sizeof(source));
36-+ isc_buffer_add(&b, sizeof(source));
37-+ isc_buffer_setactive(&b, 10); /* names 'one.' and 'two.' */
38-+
39-+ /*
40-+ * We should only be able to read two names from the buffer
41-+ * as the active region has been set to cover only the first
42-+ * two.
43-+ */
44-+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL);
45-+ assert_int_equal(result, ISC_R_SUCCESS);
46-+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL);
47-+ assert_int_equal(result, ISC_R_SUCCESS);
48-+ result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL);
49-+ assert_int_not_equal(result, ISC_R_SUCCESS);
50-+}
51-+
52- /* is trust-anchor-telemetry test */
53- ISC_RUN_TEST_IMPL(istat) {
54- dns_fixedname_t fixed;
55-@@ -1009,6 +1035,7 @@ ISC_TEST_ENTRY(fullcompare)
56- ISC_TEST_ENTRY(compression)
57- ISC_TEST_ENTRY(collision)
58- ISC_TEST_ENTRY(fromregion)
59-+ISC_TEST_ENTRY(fromwire)
60- ISC_TEST_ENTRY(istat)
61- ISC_TEST_ENTRY(init)
62- ISC_TEST_ENTRY(invalidate)
63---
64-2.43.0
65- 
@@ -1,32 +0,0 @@
1-From 1911561e056dca9eba2e54de5264d61b9184f425 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 19 May 2026 15:00:17 +1000
4-Subject: [PATCH] Fix the yaml query zone name code in dnstap-read
5- 
6-When the buffer to read the query zone name was constructed
7-isc_buffer_setactive was not called. This is now needed as
8-dns_name_fromwire is being corrected to check the active region.
9- 
10-(cherry picked from commit a25522c28c46655a81d2bf1d96374c81d834b157)
11- 
12-Conflict:no
13-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/1911561e056dca9eba2e54de5264d61b9184f425
14----
15- bin/tools/dnstap-read.c | 1 +
16- 1 file changed, 1 insertion(+)
17- 
18-diff --git a/bin/tools/dnstap-read.c b/bin/tools/dnstap-read.c
19-index cbc82c33c5..a7cd1f19ab 100644
20---- a/bin/tools/dnstap-read.c
21-+++ b/bin/tools/dnstap-read.c
22-@@ -308,6 +308,7 @@ print_yaml(dns_dtdata_t *dt) {
23-
24- isc_buffer_init(&b, m->query_zone.data, m->query_zone.len);
25- isc_buffer_add(&b, m->query_zone.len);
26-+ isc_buffer_setactive(&b, m->query_zone.len);
27-
28- result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER,
29- NULL);
30---
31-2.43.0
32- 
@@ -1,34 +0,0 @@
1-From d6aa1ad5b269ab42dd18e7900ac9a0b7013aca20 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 19 May 2026 11:12:46 +1000
4-Subject: [PATCH] Fix dns_name_fromwire to honour the active region
5- 
6-dns_name_fromwire was not honouring the source buffer's active
7-region when reading names from the wire. This allowed malformed
8-records to be accepted when they shouldn't have been. This has
9-been corrected.
10- 
11-(cherry picked from commit 7c4f07a7ef6b571073327b02209df7f75b9363ff)
12- 
13-Conflict:no
14-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/d6aa1ad5b269ab42dd18e7900ac9a0b7013aca20
15----
16- lib/dns/name.c | 2 +-
17- 1 file changed, 1 insertion(+), 1 deletion(-)
18- 
19-diff --git a/lib/dns/name.c b/lib/dns/name.c
20-index 1a900fb5bc..1e8ea1a236 100644
21---- a/lib/dns/name.c
22-+++ b/lib/dns/name.c
23-@@ -1492,7 +1492,7 @@ dns_name_fromwire(dns_name_t *const name, isc_buffer_t *const source,
24- * The amount of the source we consumed is set once.
25- */
26- const uint8_t *const source_buf = isc_buffer_base(source);
27-- const uint8_t *const source_max = isc_buffer_used(source);
28-+ const uint8_t *const source_max = isc_buffer_active(source);
29- const uint8_t *const start = isc_buffer_current(source);
30- const uint8_t *marker = start;
31- const uint8_t *cursor = start;
32---
33-2.43.0
34- 
@@ -1,38 +0,0 @@
1-From 9ddc60767d4614a405b8a2c9938fe00cf60b361a Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 19 May 2026 11:24:01 +1000
4-Subject: [PATCH] Make it clearer that decompression is not allowed here
5- 
6-For DNSKEY and RRSIG as per RFC 4034, A.1.1. Private Algorithm Types
7- 
8-For KEY, SIG as per RFC 2535 3.2 The KEY Algorithm Number Specification,
9-which only allowed local domain compression which itself is no
10-longer allowed.
11- 
12-Other key types inherit from these or the general proscription that
13-compression is not allow in new types.
14- 
15-(cherry picked from commit 5e397a16dabd12c6c2459840dcd9db976d950c6c)
16- 
17-Conflict:no
18-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/9ddc60767d4614a405b8a2c9938fe00cf60b361a
19----
20- lib/dns/rdata.c | 2 +-
21- 1 file changed, 1 insertion(+), 1 deletion(-)
22- 
23-diff --git a/lib/dns/rdata.c b/lib/dns/rdata.c
24-index 42549ac4c5..bd18777be7 100644
25---- a/lib/dns/rdata.c
26-+++ b/lib/dns/rdata.c
27-@@ -591,7 +591,7 @@ check_private(isc_buffer_t *source, dns_secalg_t alg) {
28- dns_fixedname_t fixed;
29-
30- RETERR(dns_name_fromwire(dns_fixedname_initname(&fixed), source,
31-- DNS_DECOMPRESS_DEFAULT, NULL));
32-+ DNS_DECOMPRESS_NEVER, NULL));
33- } else if (alg == DNS_KEYALG_PRIVATEOID) {
34- /*
35- * Check that we can extract the OID from the start of the
36---
37-2.43.0
38- 
@@ -1,50 +0,0 @@
1-From 6fc91ef40bebc2b5e98474fa3bad57d04e8b494a Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 19 May 2026 12:03:23 +1000
4-Subject: [PATCH] Check that a short PRIVATEDNS record is rejected
5- 
6-A bug in dns_name_fromwire meant that short PRIVATEDNS key
7-records where being accepted. Test that this is no longer
8-the case.
9- 
10-(cherry picked from commit f48d48027384d8c2210b5ce9e3eac7af101ead3d)
11- 
12-Conflict:no
13-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/6fc91ef40bebc2b5e98474fa3bad57d04e8b494a
14----
15- tests/dns/rdata_test.c | 19 +++++++++++++++++++
16- 1 file changed, 19 insertions(+)
17- 
18-diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c
19-index 7af9c56329..13d790b456 100644
20---- a/tests/dns/rdata_test.c
21-+++ b/tests/dns/rdata_test.c
22-@@ -2276,6 +2276,25 @@ ISC_RUN_TEST_IMPL(key) {
23-
24- check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in,
25- dns_rdatatype_key, sizeof(dns_rdata_key_t));
26-+
27-+ /*
28-+ * A valid PRIVATEDNS record with an active region shorter than the
29-+ * actual record length. A bug in dns_name_fromwire meant that this
30-+ * was previously accepted.
31-+ */
32-+ unsigned char key[] = { 0x00, 0x00, 0x00, 253, 0x07, 'e', 'x',
33-+ 'a', 'm', 'p', 'l', 'e', 0x00 };
34-+ unsigned char buf[sizeof(key)];
35-+ isc_buffer_t source, target;
36-+ isc_result_t result;
37-+
38-+ isc_buffer_init(&source, key, sizeof(key));
39-+ isc_buffer_add(&source, sizeof(key));
40-+ isc_buffer_setactive(&source, sizeof(key) - 1);
41-+ isc_buffer_init(&target, buf, sizeof(buf));
42-+ result = dns_rdata_fromwire(NULL, dns_rdataclass_in, dns_rdatatype_key,
43-+ &source, DNS_DECOMPRESS_ALWAYS, &target);
44-+ assert_int_not_equal(result, ISC_R_SUCCESS);
45- }
46-
47- /*
48---
49-2.43.0
50- 
@@ -1,120 +0,0 @@
1-From 9a82af76e8b7249f512dd50a9a7925270475a22f Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Fri, 5 Jun 2026 09:18:30 +1000
4-Subject: [PATCH] POC for PRIVATEDNS DNSKEY overrun not being detected
5- 
6-Construct a DNS message where a PRIVATEDNS DNSKEY identifier
7-overruns the record boundary by 3 byte so that the label ends
8-at the end of the compression pointer for the next record. The
9-next type is less than 256 so the next octet is 00 terminating
10-the identifier name. The transfered zone is then written to
11-disk using master-format text triggering the assertion when the
12-truncated identier is discovered.
13- 
14-Note this test will produce a false result in versions of
15-BIND that do not check the PRIVATEDNS identifier as it looks
16-for the error message when the transfer is aborted.
17- 
18-(cherry picked from commit 9ce3bce8bc8b4e9c6a9b1e84b5849c33eb27830e)
19- 
20-Conflict:context adapte
21-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/9a82af76e8b7249f512dd50a9a7925270475a22f
22----
23- bin/tests/system/xfer/ans9/ans.py | 31 ++++++++++++++++++++++++-
24- bin/tests/system/xfer/ns6/named.conf.j2 | 9 +++++++
25- bin/tests/system/xfer/tests_xfer.py | 9 +++++++
26- 3 files changed, 48 insertions(+), 1 deletion(-)
27- 
28-diff --git a/bin/tests/system/xfer/ans9/ans.py b/bin/tests/system/xfer/ans9/ans.py
29-index 25da6b7..a9e7395 100644
30---- a/bin/tests/system/xfer/ans9/ans.py
31-+++ b/bin/tests/system/xfer/ans9/ans.py
32-@@ -13,6 +13,7 @@ information regarding copyright ownership.
33-
34- from collections.abc import AsyncGenerator
35-
36-+import dns.name
37- import dns.rcode
38- import dns.rdatatype
39- import dns.rrset
40-@@ -33,7 +34,7 @@ class AXFRServer(DomainHandler):
41- version.
42- """
43-
44-- domains = ["xfr-and-reconfig"]
45-+ domains = ["xfr-and-reconfig", "private-dns-overrun"]
46-
47- def __init__(self) -> None:
48- super().__init__()
49-@@ -97,6 +98,34 @@ class AXFRServer(DomainHandler):
50-
51- yield DnsResponseSend(txt_message)
52-
53-+ if qctx.qname == dns.name.from_text("private-dns-overrun"):
54-+ # A message where the malformed DNSKEY algorithm identifier
55-+ # finishes on a 00 byte in the next record. Assumes the
56-+ # next record starts with a compression pointer which is
57-+ # followed by the type which starts with 00.
58-+
59-+ # Generate malformed PRIVATE DNS DNSKEY
60-+ dnskey_message = qctx.prepare_new_response()
61-+ dnskey_rrset = dns.rrset.from_text(
62-+ qctx.qname,
63-+ 300,
64-+ qctx.qclass,
65-+ dns.rdatatype.DNSKEY,
66-+ "\\# 12 00 00 00 fd 09 00 00 00 00 00 00 00",
67-+ )
68-+ dnskey_message.answer.append(dnskey_rrset)
69-+ # Generate well formed PRIVATE DNS DNSKEY
70-+ dnskey_rrset = dns.rrset.from_text(
71-+ qctx.qname,
72-+ 300,
73-+ qctx.qclass,
74-+ dns.rdatatype.DNSKEY,
75-+ "\\# 12 00 00 00 fd 06 00 00 00 00 00 00 00",
76-+ )
77-+ dnskey_message.answer.append(dnskey_rrset)
78-+
79-+ yield DnsResponseSend(dnskey_message)
80-+
81- # Finish the AXFR transaction by sending the second SOA RRset.
82- yield DnsResponseSend(soa_message)
83-
84-diff --git a/bin/tests/system/xfer/ns6/named.conf.j2 b/bin/tests/system/xfer/ns6/named.conf.j2
85-index 08ca01f..1f2292a 100644
86---- a/bin/tests/system/xfer/ns6/named.conf.j2
87-+++ b/bin/tests/system/xfer/ns6/named.conf.j2
88-@@ -111,3 +111,12 @@ zone "xfr-and-reconfig" {
89- file "xfr-and-reconfig.bk";
90- request-ixfr no; # ans9 supports only axfr
91- };
92-+
93-+# GL#6004
94-+zone "private-dns-overrun" {
95-+ type secondary;
96-+ primaries { 10.53.0.9; };
97-+ file "private-dns-overrun.bk";
98-+ masterfile-format text; # force bug to be exercised
99-+ request-ixfr no; # ans9 supports only axfr
100-+};
101-diff --git a/bin/tests/system/xfer/tests_xfer.py b/bin/tests/system/xfer/tests_xfer.py
102-index 78b9017..f7b9905 100644
103---- a/bin/tests/system/xfer/tests_xfer.py
104-+++ b/bin/tests/system/xfer/tests_xfer.py
105-@@ -549,3 +549,12 @@ def test_reconfiguration_when_zone_transfer_is_in_the_middle_of_soa_query(ns6):
106- isctest.log.info("Try to reload the zone from the primary")
107- ns6.rndc("reload xfr-and-reconfig")
108- watcher_transfer_started.wait_for_line("Transfer started")
109-+
110-+def test_malformed_private_dns_identifier_overrun(ns6):
111-+ isctest.log.info(
112-+ "Check that a malformed PRIVATEDNS DNSKEY which overruns the record is rejected"
113-+ )
114-+ with ns6.watch_log_from_start(timeout=60) as watcher_transfer_completed:
115-+ watcher_transfer_completed.wait_for_line(
116-+ "zone private-dns-overrun/IN: zone transfer finished: unexpected end of input"
117-+ )
118---
119-2.43.0
120- 
@@ -1,33 +0,0 @@
1-From 4167d965330232d0499e61db0b13d9d0a97e64da Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Fri, 10 Apr 2026 10:26:14 +1000
4-Subject: [PATCH] Properly handle rpz name to long wildcard expansion
5- 
6-Previously a self referential CNAME and the original address
7-record were returned. We now return a YXDOMAIN response.
8- 
9-(cherry picked from commit cfc4c4f69870ce492deaaa429453563d1621ded3)
10- 
11-Conflict:no
12-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/4167d965330232d0499e61db0b13d9d0a97e64da
13----
14- lib/ns/query.c | 3 ++-
15- 1 file changed, 2 insertions(+), 1 deletion(-)
16- 
17-diff --git a/lib/ns/query.c b/lib/ns/query.c
18-index 0e4396a228..6d31570f3e 100644
19---- a/lib/ns/query.c
20-+++ b/lib/ns/query.c
21-@@ -7334,7 +7334,8 @@ query_rpzcname(query_ctx_t *qctx, dns_name_t *cname) {
22- qctx->fname, NULL);
23- if (result == DNS_R_NAMETOOLONG) {
24- client->message->rcode = dns_rcode_yxdomain;
25-- } else if (result != ISC_R_SUCCESS) {
26-+ }
27-+ if (result != ISC_R_SUCCESS) {
28- return result;
29- }
30- } else {
31---
32-2.43.0
33- 
@@ -1,29 +0,0 @@
1-From c392d9958b02d6b82c5566f5d499d87a1297b854 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Fri, 10 Apr 2026 10:24:06 +1000
4-Subject: [PATCH] Fix TTL extraction from A/AAAA record
5- 
6-(cherry picked from commit 89c86e338db2492b92e6618c586f146c6928dc6d)
7- 
8-Conflict:no
9-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/c392d9958b02d6b82c5566f5d499d87a1297b854
10----
11- bin/tests/system/rpz/tests.sh | 2 +-
12- 1 file changed, 1 insertion(+), 1 deletion(-)
13- 
14-diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh
15-index f15215a43c..bdcc06de53 100644
16---- a/bin/tests/system/rpz/tests.sh
17-+++ b/bin/tests/system/rpz/tests.sh
18-@@ -419,7 +419,7 @@ addr() {
19- digcmd $2 >$DIGNM
20- #ckalive "$2" "server crashed by 'dig $2'" || return 1
21- ADDR_ESC=$(echo "$ADDR" | sed -e 's/\./\\./g')
22-- ADDR_TTL=$(sed -n -e "s/^[-.a-z0-9]\{1,\}[ ]*\([0-9]*\) IN AA* ${ADDR_ESC}\$/\1/p" $DIGNM)
23-+ ADDR_TTL=$(sed -n -e "s/^[-.a-z0-9]\{1,\}[ ]*\([0-9]*\)[ ]IN[ ]AA*[ ]${ADDR_ESC}\$/\1/p" $DIGNM)
24- if test -z "$ADDR_TTL"; then
25- setret "'dig $2' wrong; no address $ADDR record in $DIGNM"
26- return 0
27---
28-2.43.0
29- 
@@ -1,57 +0,0 @@
1-From 1009cae26631efdbeab1a452462d12b73eef69f4 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Fri, 10 Apr 2026 10:24:40 +1000
4-Subject: [PATCH] Check rpz name too long wildcard CNAME expansion handling
5- 
6-(cherry picked from commit 9345394e2097031b55b3ef34ceaadf5a7ebbeef2)
7- 
8-Conflict:no
9-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/1009cae26631efdbeab1a452462d12b73eef69f4
10----
11- bin/tests/system/rpz/ns2/tld2.db | 2 ++
12- bin/tests/system/rpz/ns4/tld4.db | 2 ++
13- bin/tests/system/rpz/tests.sh | 5 ++++-
14- 3 files changed, 8 insertions(+), 1 deletion(-)
15- 
16-diff --git a/bin/tests/system/rpz/ns2/tld2.db b/bin/tests/system/rpz/ns2/tld2.db
17-index a66ee16d14..fbc7b8318a 100644
18---- a/bin/tests/system/rpz/ns2/tld2.db
19-+++ b/bin/tests/system/rpz/ns2/tld2.db
20-@@ -126,3 +126,5 @@ a7-2 A 192.168.7.2
21-
22- a8-1 A 192.168.8.1
23- TXT "a8-1 tld2 text"
24-+
25-+*.wild A 192.168.9.1
26-diff --git a/bin/tests/system/rpz/ns4/tld4.db b/bin/tests/system/rpz/ns4/tld4.db
27-index e65dd94a33..d6b5d98946 100644
28---- a/bin/tests/system/rpz/ns4/tld4.db
29-+++ b/bin/tests/system/rpz/ns4/tld4.db
30-@@ -59,6 +59,8 @@ a3-6.tld2 A 56.56.56.56
31-
32- a3-7.sub1.tld2 A 57.57.57.57
33-
34-+*.wild.sub1.tld2 A 57.57.57.57
35-+
36- a3-8.tld2 A 58.58.58.58
37-
38- a3-9.sub9.tld2 A 59.59.59.59
39-diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh
40-index bdcc06de53..ca704e11e2 100644
41---- a/bin/tests/system/rpz/tests.sh
42-+++ b/bin/tests/system/rpz/tests.sh
43-@@ -545,7 +545,10 @@ nochange TCP a3-9.tld2 # 33 tcp-only
44- here x.servfail <<'EOF' # 34 qname-wait-recurse yes
45- ;; status: SERVFAIL, x
46- EOF
47--addr 35.35.35.35 "x.servfail @$ns5" # 35 qname-wait-recurse no
48-+addr 35.35.35.35 "x.servfail @$ns5" # 35 qname-wait-recurse no
49-+here aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.wild.sub1.tld2 <<'EOF' # 36 wildcard CNAME name to long
50-+ ;; status: YXDOMAIN, x
51-+EOF
52- end_group
53- ckstats $ns3 test1 ns3 22
54- ckstats $ns5 test1 ns5 1
55---
56-2.43.0
57- 
@@ -1,42 +0,0 @@
1-From e33d61bf7494a9a9399a7111f0b3b7f44fb9e749 Mon Sep 17 00:00:00 2001
2-From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= <ondrej@sury.org>
3-Date: Sun, 7 Jun 2026 10:19:44 +0200
4-Subject: [PATCH] Limit DNSSEC denial proof validation per fetch
5- 
6-Negative response validation reuses the per-fetch validation counters,
7-but NSEC/NSEC3 subvalidator quota failures were not terminal. The
8-parent validator logged the child failure and resumed validate_nx(),
9-so a response stuffed with many denial proof RRsets could keep driving
10-more validation work and still eventually validate.
11- 
12-Treat ISC_R_QUOTA from an NSEC/NSEC3 subvalidator as terminal and
13-propagate the quota reason to the parent validator. This lets the
14-existing fctx->nvalidations and fctx->nfails limits stop excessive
15-negative proof validation without a separate resolver-side pre-scan.
16- 
17-(cherry picked from commit c3388378a7699c956603bc405f73e38d2c344dc8)
18- 
19-Conflict:no
20-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e33d61bf7494a9a9399a7111f0b3b7f44fb9e749
21----
22- lib/dns/validator.c | 4 ++++
23- 1 file changed, 4 insertions(+)
24- 
25-diff --git a/lib/dns/validator.c b/lib/dns/validator.c
26-index 84fbfd5789..69176462e3 100644
27---- a/lib/dns/validator.c
28-+++ b/lib/dns/validator.c
29-@@ -911,6 +911,10 @@ validator_callback_nsec(void *arg) {
30- switch (eresult) {
31- case ISC_R_CANCELED:
32- case ISC_R_SHUTTINGDOWN:
33-+ case ISC_R_QUOTA:
34-+ val->attributes |= subvalidator->attributes &
35-+ (VALATTR_MAXVALIDATIONS |
36-+ VALATTR_MAXVALIDATIONFAILS);
37- result = eresult;
38- break;
39- case DNS_R_BROKENCHAIN:
40---
41-2.43.0
42- 
@@ -1,623 +0,0 @@
1-From 2dabf117e1264fd13fb33096f87e78a039fd1c6c Mon Sep 17 00:00:00 2001
2-From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= <ondrej@sury.org>
3-Date: Tue, 23 Jun 2026 10:59:38 +0200
4-Subject: [PATCH] Make the dns_slabheaders in the cache reference counted
5- 
6-Instead of only reference counting the enclosing qpcnode, add the
7-reference counting directly to the slabheaders. The reference is
8-incremented when an rdataset is bound to the header and decremented when
9-the rdataset is disassociated, so a stale slabheader can be removed from
10-the node's down chain as soon as its own reference count reaches zero,
11-instead of waiting for the whole qpcnode to become unreferenced.
12- 
13-Building on that, clean up the ancient headers eagerly: mark_ancient()
14-is made idempotent, releases the header's own (container) reference and
15-reaps the stale headers from the node's down chain as soon as their
16-references reach zero. A header evicted over the per-name type limit is
17-expired only after the new rdataset has been bound, so the bind's
18-increment always precedes mark_ancient()'s decrement.
19- 
20-Because a header can now be reclaimed independently of its node, the
21-rdataset iterators must keep the header they are positioned on alive:
22-each iterator takes a reference on its current header and releases it
23-when it advances or is destroyed. Iteration otherwise stays lazy and
24-re-reads the node on every step, so it still observes records added to
25-the node while the iterator is live, as zone signing requires.
26- 
27-The slab headers are shared with the zone databases, so the matching
28-increment is added to every bind path. The noqname/closest proofs hand
29-out rdatasets backed by bare slabs that have no header, so they are
30-given a separate dns_rdataproof_rdatasetmethods that leaves the
31-reference count untouched.
32- 
33-Conflict:no
34-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/2dabf117e1264fd13fb33096f87e78a039fd1c6c
35----
36- bin/tests/system/reclimit/tests.sh | 4 +-
37- lib/dns/include/dns/rdataslab.h | 3 ++
38- lib/dns/qpcache.c | 67 ++++++++++++++++++++++------
39- lib/dns/qpzone.c | 25 +++++++++++
40- lib/dns/rbt-cachedb.c | 16 ++-----
41- lib/dns/rbtdb.c | 71 +++++++++++++++++++++++-------
42- lib/dns/rbtdb_p.h | 2 +
43- lib/dns/rdataslab.c | 35 +++++++++++++--
44- 8 files changed, 174 insertions(+), 49 deletions(-)
45- 
46-diff --git a/bin/tests/system/reclimit/tests.sh b/bin/tests/system/reclimit/tests.sh
47-index e7510886cb..698251090a 100644
48---- a/bin/tests/system/reclimit/tests.sh
49-+++ b/bin/tests/system/reclimit/tests.sh
50-@@ -341,13 +341,13 @@ echo_i "checking that NXDOMAIN names over the max-types-per-name limit don't get
51-
52- # Query for 10 NXDOMAIN types
53- for ntype in $(seq 65270 65279); do
54-- check_manytypes 1 manytypes.big "TYPE${ntype}" NOERROR big SOA 0 || ret=1
55-+ check_manytypes 1 manytypes.big "TYPE${ntype}" NOERROR big SOA 120 || ret=1
56- done
57- # Wait at least 1 second
58- sleep 1
59- # Query for 10 NXDOMAIN types again - these should not be cached
60- for ntype in $(seq 65270 65279); do
61-- check_manytypes 2 manytypes.big "TYPE${ntype}" NOERROR big SOA 0 || ret=1
62-+ check_manytypes 2 manytypes.big "TYPE${ntype}" NOERROR big SOA 120 || ret=1
63- done
64-
65- if [ $ret -ne 0 ]; then echo_i "failed"; fi
66-diff --git a/lib/dns/include/dns/rdataslab.h b/lib/dns/include/dns/rdataslab.h
67-index 8fc68322ca..6eeb55d24a 100644
68---- a/lib/dns/include/dns/rdataslab.h
69-+++ b/lib/dns/include/dns/rdataslab.h
70-@@ -46,6 +46,7 @@
71- #include <isc/atomic.h>
72- #include <isc/heap.h>
73- #include <isc/lang.h>
74-+#include <isc/refcount.h>
75- #include <isc/stdtime.h>
76- #include <isc/urcu.h>
77-
78-@@ -95,6 +96,8 @@ struct dns_slabheader {
79- isc_stdtime_t last_used;
80- _Atomic(uint32_t) last_refresh_fail_ts;
81-
82-+ isc_refcount_t references;
83-+
84- dns_slabheader_proof_t *noqname;
85- dns_slabheader_proof_t *closest;
86- /*%<
87-diff --git a/lib/dns/qpcache.c b/lib/dns/qpcache.c
88-index b0ff0f50d5..7295e47e33 100644
89---- a/lib/dns/qpcache.c
90-+++ b/lib/dns/qpcache.c
91-@@ -592,13 +592,19 @@ update_header(qpcache_t *qpdb, dns_slabheader_t *header, isc_stdtime_t now) {
92-
93- static void
94- clean_stale_headers(dns_slabheader_t *top) {
95-- dns_slabheader_t *d = NULL, *down_next = NULL;
96-+ dns_slabheader_t *d = NULL, *down_next = NULL, *down_parent = top;
97-
98- for (d = top->down; d != NULL; d = down_next) {
99- down_next = d->down;
100-- dns_slabheader_destroy(&d);
@@ -1,35 +0,0 @@
1-From dc1436fa28315a27b5185e4163cd0fa877bda3e4 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 14 Apr 2026 15:14:06 +1000
4-Subject: [PATCH] Don't sign out of zone records in dnssec-signzone
5- 
6-dnssec-signzone was signing extraneous records that were not within
7-the namespace of the zone. This no longer occurs.
8- 
9-(cherry picked from commit e45c9af7051421fd370f20ba8325199c606223fd)
10- 
11-Conflict:no
12-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/dc1436fa28315a27b5185e4163cd0fa877bda3e4
13----
14- bin/dnssec/dnssec-signzone.c | 5 +++++
15- 1 file changed, 5 insertions(+)
16- 
17-diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c
18-index c7c0798088..63234040d4 100644
19---- a/bin/dnssec/dnssec-signzone.c
20-+++ b/bin/dnssec/dnssec-signzone.c
21-@@ -1623,6 +1623,11 @@ assignwork(void *arg) {
22- dns_db_detachnode(gdb, &node);
23- goto next;
24- }
25-+ if (!dns_name_issubdomain(name, gorigin)) {
26-+ dumpnode(name, node);
27-+ dns_db_detachnode(gdb, &node);
28-+ goto next;
29-+ }
30- /*
31- * Sort the zone data from the glue and out-of-zone data.
32- * For NSEC zones nodes with zone data have NSEC records.
33---
34-2.43.0
35- 
@@ -1,219 +0,0 @@
1-From 8a46533cbeab78c215ee035d73967a3a59925375 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 14 Apr 2026 12:24:33 +1000
4-Subject: [PATCH] Invalid signed wildcard records were being accepted
5- 
6-An RRSIG whose Labels field indicates fewer labels than its signer
7-name requires was being accepted. When such a record covers a
8-wildcard, the validator reconstructs a wildcard owner name above the
9-signer's zone and caches it as secure. RFC 8198 cache synthesis
10-(synth-from-dnssec) then serves that forged wildcard for unrelated
11-names, poisoning the cache.
12- 
13-These records are now rejected, both when an RRSIG is parsed and when
14-its signature is verified.
15- 
16-(cherry picked from commit 084ca5ee10515e461d46b63df9660b8394bc7de9)
17- 
18-Conflict:no
19-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/8a46533cbeab78c215ee035d73967a3a59925375
20----
21- lib/dns/dnssec.c | 39 ++++++++++++++++++++++++--------
22- lib/dns/rdata/generic/rrsig_46.c | 32 ++++++++++++++++++++------
23- 2 files changed, 54 insertions(+), 17 deletions(-)
24- 
25-diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
26-index aa664b9187..4b5ddc2184 100644
27---- a/lib/dns/dnssec.c
28-+++ b/lib/dns/dnssec.c
29-@@ -129,7 +129,7 @@ dns_dnssec_keyfromrdata(const dns_name_t *name, const dns_rdata_t *rdata,
30- isc_buffer_t b;
31- isc_region_t r;
32-
33-- INSIST(name != NULL);
34-+ INSIST(DNS_NAME_VALID(name));
35- INSIST(rdata != NULL);
36- INSIST(mctx != NULL);
37- INSIST(key != NULL);
38-@@ -186,12 +186,14 @@ dns_dnssec_sign(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
39- isc_result_t ret;
40- isc_buffer_t *databuf = NULL;
41- char data[256 + 8];
42-+ unsigned int labels;
43- unsigned int sigsize;
44- dns_fixedname_t fnewname;
45- dns_fixedname_t fsigner;
46-
47-- REQUIRE(name != NULL);
48-- REQUIRE(dns_name_countlabels(name) <= 255);
49-+ REQUIRE(DNS_NAME_VALID(name));
50-+ labels = dns_name_countlabels(name);
51-+ REQUIRE(labels <= 255 && labels > 0);
52- REQUIRE(set != NULL);
53- REQUIRE(key != NULL);
54- REQUIRE(inception != NULL);
55-@@ -220,7 +222,7 @@ dns_dnssec_sign(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
56-
57- sig.covered = set->type;
58- sig.algorithm = dst_key_alg(key);
59-- sig.labels = dns_name_countlabels(name) - 1;
60-+ sig.labels = labels - 1;
61- if (dns_name_iswildcard(name)) {
62- sig.labels--;
63- }
64-@@ -364,10 +366,13 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
65- isc_result_t ret;
66- unsigned char data[300];
67- dst_context_t *ctx = NULL;
68-- int labels = 0;
69-+ unsigned int labels;
70-+ unsigned int siglabels;
71- bool downcase = false;
72-
73-- REQUIRE(name != NULL);
74-+ REQUIRE(DNS_NAME_VALID(name));
75-+ labels = dns_name_countlabels(name);
76-+ REQUIRE(labels > 0);
77- REQUIRE(set != NULL);
78- REQUIRE(key != NULL);
79- REQUIRE(mctx != NULL);
80-@@ -382,6 +387,21 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
81- return DNS_R_SIGINVALID;
82- }
83-
84-+ /*
85-+ * The RRSIG labels field can't indicate fewer labels than the
86-+ * signer. Also the labels shouldn't be greater than that of
87-+ * the owner name.
88-+ *
89-+ * sig.labels doesn't include the root label, so add 1 to account
90-+ * for it.
91-+ */
92-+ siglabels = sig.labels + 1;
93-+ if (siglabels < dns_name_countlabels(&sig.signer) || siglabels > labels)
94-+ {
95-+ inc_stat(dns_dnssecstats_fail);
96-+ return DNS_R_SIGINVALID;
97-+ }
98-+
99- if (isc_serial_lt(sig.timeexpire, sig.timesigned)) {
100- inc_stat(dns_dnssecstats_fail);
101- return DNS_R_SIGINVALID;
102-@@ -448,10 +468,9 @@ again:
103- * If the name is an expanded wildcard, use the wildcard name.
104- */
105- dns_fixedname_init(&fnewname);
106-- labels = dns_name_countlabels(name) - 1;
107- RUNTIME_CHECK(dns_name_downcase(name, dns_fixedname_name(&fnewname),
108- NULL) == ISC_R_SUCCESS);
109-- if (labels - sig.labels > 0) {
110-+ if (labels > siglabels) {
111- dns_name_split(dns_fixedname_name(&fnewname), sig.labels + 1,
112- NULL, dns_fixedname_name(&fnewname));
113- }
114-@@ -462,7 +481,7 @@ again:
115- * Create an envelope for each rdata: <name|type|class|ttl>.
116- */
117- isc_buffer_init(&envbuf, data, sizeof(data));
118-- if (labels - sig.labels > 0) {
119-+ if (labels > siglabels) {
120- isc_buffer_putuint8(&envbuf, 1);
121- isc_buffer_putuint8(&envbuf, '*');
122- memmove(data + 2, r.base, r.length);
123-@@ -557,7 +576,7 @@ cleanup_struct:
124- inc_stat(dns_dnssecstats_fail);
125- }
126-
127-- if (ret == ISC_R_SUCCESS && labels - sig.labels > 0) {
128-+ if (ret == ISC_R_SUCCESS && labels > siglabels) {
129- if (wild != NULL) {
130- RUNTIME_CHECK(dns_name_concatenate(
131- dns_wildcardname,
132-diff --git a/lib/dns/rdata/generic/rrsig_46.c b/lib/dns/rdata/generic/rrsig_46.c
133-index 80b6fbc711..75ea2e8e90 100644
134---- a/lib/dns/rdata/generic/rrsig_46.c
135-+++ b/lib/dns/rdata/generic/rrsig_46.c
136-@@ -23,12 +23,12 @@
137- static isc_result_t
138- fromtext_rrsig(ARGS_FROMTEXT) {
139- isc_token_t token;
140-- unsigned char alg, c;
141-+ unsigned char alg, labels;
142- long i;
143- dns_rdatatype_t covered;
144-- char *e;
145-+ char *e = NULL;
146- isc_result_t result;
147-- dns_name_t name;
148-+ dns_name_t signer;
149- isc_buffer_t buffer;
150- uint32_t time_signed, time_expire;
151- unsigned int used;
152-@@ -73,8 +73,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
153- if (token.value.as_ulong > 0xffU) {
154- RETTOK(ISC_R_RANGE);
155- }
156-- c = (unsigned char)token.value.as_ulong;
157-- RETERR(mem_tobuffer(target, &c, 1));
158-+ labels = (unsigned char)token.value.as_ulong;
159-+ RETERR(mem_tobuffer(target, &labels, 1));
160-
161- /*
162- * Original ttl.
163-@@ -145,12 +145,20 @@ fromtext_rrsig(ARGS_FROMTEXT) {
164- */
165- RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
166- false));
167-- dns_name_init(&name, NULL);
168-+ dns_name_init(&signer, NULL);
169- buffer_fromregion(&buffer, &token.value.as_region);
170- if (origin == NULL) {
171- origin = dns_rootname;
172- }
173-- RETTOK(dns_name_fromtext(&name, &buffer, origin, options, target));
174-+ RETTOK(dns_name_fromtext(&signer, &buffer, origin, options, target));
175-+
176-+ /*
177-+ * (RRSIG labels doesn't include the root label, so add one
178-+ * to normalize it before checking against the signer.)
179-+ */
180-+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&signer)) {
181-+ RETTOK(ISC_R_RANGE);
182-+ }
183-
184- /*
185- * Sig.
186-@@ -297,6 +305,7 @@ fromwire_rrsig(ARGS_FROMWIRE) {
187- isc_region_t sr;
188- dns_name_t name;
189- unsigned char algorithm;
190-+ unsigned char labels;
191-
192- REQUIRE(type == dns_rdatatype_rrsig);
193-
194-@@ -320,6 +329,7 @@ fromwire_rrsig(ARGS_FROMWIRE) {
195- }
196-
197- algorithm = sr.base[2];
198-+ labels = sr.base[3];
199-
200- isc_buffer_forward(source, 18);
201- RETERR(mem_tobuffer(target, sr.base, 18));
202-@@ -330,6 +340,14 @@ fromwire_rrsig(ARGS_FROMWIRE) {
203- dns_name_init(&name, NULL);
204- RETERR(dns_name_fromwire(&name, source, dctx, target));
205-
206-+ /*
207-+ * (RRSIG labels doesn't include the root label, so add one
208-+ * to normalize it before checking against the signer.)
209-+ */
210-+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&name)) {
211-+ RETERR(DNS_R_FORMERR);
212-+ }
213-+
214- /*
215- * Sig.
216- */
217---
218-2.43.0
219- 
@@ -1,146 +0,0 @@
1-From 34e69230a035287195a1fd4413cf4901eb0e2ee2 Mon Sep 17 00:00:00 2001
2-From: Mark Andrews <marka@isc.org>
3-Date: Tue, 14 Apr 2026 13:46:22 +1000
4-Subject: [PATCH] Test RRSIG record parsing
5- 
6-In particular test that labels and signer fields are consistent.
7- 
8-(cherry picked from commit 5a95e64731afe63d348d272cc4d3b2f9847150c2)
9- 
10-Conflict:no
11-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/34e69230a035287195a1fd4413cf4901eb0e2ee2
12----
13- tests/dns/rdata_test.c | 110 +++++++++++++++++++++++++++++++++++++++++
14- 1 file changed, 110 insertions(+)
15- 
16-diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c
17-index 13d790b456..9938f5a816 100644
18---- a/tests/dns/rdata_test.c
19-+++ b/tests/dns/rdata_test.c
20-@@ -2676,6 +2676,115 @@ ISC_RUN_TEST_IMPL(rkey) {
21- dns_rdatatype_rkey, sizeof(dns_rdata_rkey_t));
22- }
23-
24-+ISC_RUN_TEST_IMPL(rrsig) {
25-+ text_ok_t text_ok[] = {
26-+ TEXT_VALID("SOA 8 0 86400 20260426170000 20260413160000 54393 "
27-+ ". "
28-+ "tFbcoVP8MnpecUquJ/aj+XeNgV7ts9GSHVkXaXRJrJ/"
29-+ "TEkOZApVG0F6E "
30-+ "9sYpxGk2ItweLL43ujioGj0HWwZDRR+vbur+O/"
31-+ "dIdheiig1VvU+9HXLi "
32-+ "QOViY9Kc64ixdyJhYCC5K+bO1qsHxd+"
33-+ "KJXOaxyHbqchYkDFy4PL6qftE "
34-+ "VaLkueRgjXgOsq/"
35-+ "NxvCXDgAa5xy0+3Sl0myxIs8rJ5KeXfJQFe7qxgaw "
36-+ "VjJsJTKw8neOTw2rQfLaigWu2LIWw+"
37-+ "IyVrLjZJdLqGkiLBGd1w4X3U12 "
38-+ "fFxoY3eqzNgBEtduoGKPZ/"
39-+ "NpP9cuKJORJ18283aV8hR4WO91VR0q1zcM jLwqUg=="),
40-+ /* labels too short for signer */
41-+ TEXT_INVALID("SOA 8 0 86400 20260426170000 20260413160000 "
42-+ "54393 example. "
43-+ "tFbcoVP8MnpecUquJ/aj+XeNgV7ts9GSHVkXaXRJrJ/"
44-+ "TEkOZApVG0F6E "
45-+ "9sYpxGk2ItweLL43ujioGj0HWwZDRR+vbur+O/"
46-+ "dIdheiig1VvU+9HXLi "
47-+ "QOViY9Kc64ixdyJhYCC5K+bO1qsHxd+"
48-+ "KJXOaxyHbqchYkDFy4PL6qftE "
49-+ "VaLkueRgjXgOsq/"
50-+ "NxvCXDgAa5xy0+3Sl0myxIs8rJ5KeXfJQFe7qxgaw "
51-+ "VjJsJTKw8neOTw2rQfLaigWu2LIWw+"
52-+ "IyVrLjZJdLqGkiLBGd1w4X3U12 "
53-+ "fFxoY3eqzNgBEtduoGKPZ/"
54-+ "NpP9cuKJORJ18283aV8hR4WO91VR0q1zcM jLwqUg=="),
55-+ /*
56-+ * Sentinel.
57-+ */
58-+ TEXT_SENTINEL()
59-+ };
60-+ wire_ok_t wire_ok[] = {
61-+ WIRE_VALID(0x00, 0x06, 0x08, 0x00, 0x00, 0x01, 0x51, 0x80, 0x69,
62-+ 0xee, 0x44, 0x90, 0x69, 0xdd, 0x13, 0x00, 0xd4, 0x79,
63-+ 0x00, 0xb4, 0x56, 0xdc, 0xa1, 0x53, 0xfc, 0x32, 0x7a,
64-+ 0x5e, 0x71, 0x4a, 0xae, 0x27, 0xf6, 0xa3, 0xf9, 0x77,
65-+ 0x8d, 0x81, 0x5e, 0xed, 0xb3, 0xd1, 0x92, 0x1d, 0x59,
66-+ 0x17, 0x69, 0x74, 0x49, 0xac, 0x9f, 0xd3, 0x12, 0x43,
67-+ 0x99, 0x02, 0x95, 0x46, 0xd0, 0x5e, 0x84, 0xf6, 0xc6,
68-+ 0x29, 0xc4, 0x69, 0x36, 0x22, 0xdc, 0x1e, 0x2c, 0xbe,
69-+ 0x37, 0xba, 0x38, 0xa8, 0x1a, 0x3d, 0x07, 0x5b, 0x06,
70-+ 0x43, 0x45, 0x1f, 0xaf, 0x6e, 0xea, 0xfe, 0x3b, 0xf7,
71-+ 0x48, 0x76, 0x17, 0xa2, 0x8a, 0x0d, 0x55, 0xbd, 0x4f,
72-+ 0xbd, 0x1d, 0x72, 0xe2, 0x40, 0xe5, 0x62, 0x63, 0xd2,
73-+ 0x9c, 0xeb, 0x88, 0xb1, 0x77, 0x22, 0x61, 0x60, 0x20,
74-+ 0xb9, 0x2b, 0xe6, 0xce, 0xd6, 0xab, 0x07, 0xc5, 0xdf,
75-+ 0x8a, 0x25, 0x73, 0x9a, 0xc7, 0x21, 0xdb, 0xa9, 0xc8,
76-+ 0x58, 0x90, 0x31, 0x72, 0xe0, 0xf2, 0xfa, 0xa9, 0xfb,
77-+ 0x44, 0x55, 0xa2, 0xe4, 0xb9, 0xe4, 0x60, 0x8d, 0x78,
78-+ 0x0e, 0xb2, 0xaf, 0xcd, 0xc6, 0xf0, 0x97, 0x0e, 0x00,
79-+ 0x1a, 0xe7, 0x1c, 0xb4, 0xfb, 0x74, 0xa5, 0xd2, 0x6c,
80-+ 0xb1, 0x22, 0xcf, 0x2b, 0x27, 0x92, 0x9e, 0x5d, 0xf2,
81-+ 0x50, 0x15, 0xee, 0xea, 0xc6, 0x06, 0xb0, 0x56, 0x32,
82-+ 0x6c, 0x25, 0x32, 0xb0, 0xf2, 0x77, 0x8e, 0x4f, 0x0d,
83-+ 0xab, 0x41, 0xf2, 0xda, 0x8a, 0x05, 0xae, 0xd8, 0xb2,
84-+ 0x16, 0xc3, 0xe2, 0x32, 0x56, 0xb2, 0xe3, 0x64, 0x97,
85-+ 0x4b, 0xa8, 0x69, 0x22, 0x2c, 0x11, 0x9d, 0xd7, 0x0e,
86-+ 0x17, 0xdd, 0x4d, 0x76, 0x7c, 0x5c, 0x68, 0x63, 0x77,
87-+ 0xaa, 0xcc, 0xd8, 0x01, 0x12, 0xd7, 0x6e, 0xa0, 0x62,
88-+ 0x8f, 0x67, 0xf3, 0x69, 0x3f, 0xd7, 0x2e, 0x28, 0x93,
89-+ 0x91, 0x27, 0x5f, 0x36, 0xf3, 0x76, 0x95, 0xf2, 0x14,
90-+ 0x78, 0x58, 0xef, 0x75, 0x55, 0x1d, 0x2a, 0xd7, 0x37,
91-+ 0x0c, 0x8c, 0xbc, 0x2a, 0x52),
92-+ /* labels too short for signer */
93-+ WIRE_INVALID(
94-+ 0x00, 0x06, 0x08, 0x00, 0x00, 0x01, 0x51, 0x80, 0x69,
95-+ 0xee, 0x44, 0x90, 0x69, 0xdd, 0x13, 0x00, 0xd4, 0x79,
96-+ 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x00,
97-+ 0xb4, 0x56, 0xdc, 0xa1, 0x53, 0xfc, 0x32, 0x7a, 0x5e,
98-+ 0x71, 0x4a, 0xae, 0x27, 0xf6, 0xa3, 0xf9, 0x77, 0x8d,
99-+ 0x81, 0x5e, 0xed, 0xb3, 0xd1, 0x92, 0x1d, 0x59, 0x17,
100-+ 0x69, 0x74, 0x49, 0xac, 0x9f, 0xd3, 0x12, 0x43, 0x99,
101-+ 0x02, 0x95, 0x46, 0xd0, 0x5e, 0x84, 0xf6, 0xc6, 0x29,
102-+ 0xc4, 0x69, 0x36, 0x22, 0xdc, 0x1e, 0x2c, 0xbe, 0x37,
103-+ 0xba, 0x38, 0xa8, 0x1a, 0x3d, 0x07, 0x5b, 0x06, 0x43,
104-+ 0x45, 0x1f, 0xaf, 0x6e, 0xea, 0xfe, 0x3b, 0xf7, 0x48,
105-+ 0x76, 0x17, 0xa2, 0x8a, 0x0d, 0x55, 0xbd, 0x4f, 0xbd,
106-+ 0x1d, 0x72, 0xe2, 0x40, 0xe5, 0x62, 0x63, 0xd2, 0x9c,
107-+ 0xeb, 0x88, 0xb1, 0x77, 0x22, 0x61, 0x60, 0x20, 0xb9,
108-+ 0x2b, 0xe6, 0xce, 0xd6, 0xab, 0x07, 0xc5, 0xdf, 0x8a,
109-+ 0x25, 0x73, 0x9a, 0xc7, 0x21, 0xdb, 0xa9, 0xc8, 0x58,
110-+ 0x90, 0x31, 0x72, 0xe0, 0xf2, 0xfa, 0xa9, 0xfb, 0x44,
111-+ 0x55, 0xa2, 0xe4, 0xb9, 0xe4, 0x60, 0x8d, 0x78, 0x0e,
112-+ 0xb2, 0xaf, 0xcd, 0xc6, 0xf0, 0x97, 0x0e, 0x00, 0x1a,
113-+ 0xe7, 0x1c, 0xb4, 0xfb, 0x74, 0xa5, 0xd2, 0x6c, 0xb1,
114-+ 0x22, 0xcf, 0x2b, 0x27, 0x92, 0x9e, 0x5d, 0xf2, 0x50,
115-+ 0x15, 0xee, 0xea, 0xc6, 0x06, 0xb0, 0x56, 0x32, 0x6c,
116-+ 0x25, 0x32, 0xb0, 0xf2, 0x77, 0x8e, 0x4f, 0x0d, 0xab,
117-+ 0x41, 0xf2, 0xda, 0x8a, 0x05, 0xae, 0xd8, 0xb2, 0x16,
118-+ 0xc3, 0xe2, 0x32, 0x56, 0xb2, 0xe3, 0x64, 0x97, 0x4b,
119-+ 0xa8, 0x69, 0x22, 0x2c, 0x11, 0x9d, 0xd7, 0x0e, 0x17,
120-+ 0xdd, 0x4d, 0x76, 0x7c, 0x5c, 0x68, 0x63, 0x77, 0xaa,
121-+ 0xcc, 0xd8, 0x01, 0x12, 0xd7, 0x6e, 0xa0, 0x62, 0x8f,
122-+ 0x67, 0xf3, 0x69, 0x3f, 0xd7, 0x2e, 0x28, 0x93, 0x91,
123-+ 0x27, 0x5f, 0x36, 0xf3, 0x76, 0x95, 0xf2, 0x14, 0x78,
124-+ 0x58, 0xef, 0x75, 0x55, 0x1d, 0x2a, 0xd7, 0x37, 0x0c,
125-+ 0x8c, 0xbc, 0x2a, 0x52),
126-+
127-+ WIRE_SENTINEL()
128-+ };
129-+ check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in,
130-+ dns_rdatatype_rrsig, sizeof(dns_rdata_rrsig_t));
131-+}
132-+
133- ISC_RUN_TEST_IMPL(resinfo) {
134- text_ok_t text_ok[] = {
135- TEXT_VALID_CHANGED("qnamemin exterr=15,16,17 "
136-@@ -3532,6 +3641,7 @@ ISC_TEST_ENTRY(nsec3)
137- ISC_TEST_ENTRY(nxt)
138- ISC_TEST_ENTRY(resinfo)
139- ISC_TEST_ENTRY(rkey)
140-+ISC_TEST_ENTRY(rrsig)
141- ISC_TEST_ENTRY(sshfp)
142- ISC_TEST_ENTRY(wallet)
143- ISC_TEST_ENTRY(wks)
144---
145-2.43.0
146- 
@@ -1,283 +0,0 @@
1-rom 773d46d58c693047a5945c8fe40512edd0ac214e Mon Sep 17 00:00:00 2001
2-From: Colin Vidal <colin@isc.org>
3-Date: Thu, 18 Jun 2026 18:17:05 +0200
4-Subject: [PATCH] Do not assert in some CNAME/DNAME queries
5- 
6-Fix a `named` crash because of a fail assertion for certains types of
7-CNAME and DNAME queries:
8- 
9-- If a client queries for a DNAME and A record to the resolver, and the
10- authoritative server responds positively to the A query but delay the
11- DNAME response and respond later negatively;
12- 
13-- If a client queries for a CNAME and A record to the resolver, and the
14- authoritative server responds positively to the A query but delay the
15- CNAME response and respond later with a self-referential CNAME.
16- 
17-The first scenario consists of sending two queries: `foo.test./DNAME`
18-and `a.foo.test./A`. The authoritative server delays the answer for
19-`foo.test./DNAME` but immediately answers the DNAME record for the
20-second query: `foo.test. DNAME bar.test.`. The resolver caches it,
21-follows the DNAME, and resolves `a.bar.test./A`. The authoritative
22-server eventually answers negatively for `foo.test./DNAME`
23-(NOERROR/NODATA, with only an SOA in the authority section). The
24-resolver pulls out the previously cached rdataset (because it has a
25-higher trust level than the received negative answer), and wrongly (this
26-is the first bug) sets the result to `DNS_R_DNAME` instead of
27-`ISC_R_SUCCESS`. The code in `ns/query.c` that handles the resolver
28-result interprets this as "this is a non-DNAME query and we got a DNAME
29-rdataset, so follow the chain". It goes into the `query_dname()`
30-function, which asserts that the qname is a subdomain of the owner name
31-in the rdataset. That assertion fails because the qname (`foo.test.`) is
32-exactly equal to the owner name of the DNAME (`foo.test.`), rather than
33-being a subdomain of it. `DNS_R_DNAME` must only be set when the qtype
34-is something other than DNAME and the resolver has obtained a DNAME that
35-needs to be followed.
36- 
37-The second scenario consists of sending two queries:
38-`cname.foo.test./CNAME` and `cname.foo.test./A`. The authoritative
39-server delays the answer for `cname.foo.test./CNAME` but immediately
40-answers the CNAME record for the second query: `cname.foo.test. CNAME
41-cname.foo.test.`. Note that the CNAME is self-referential. The resolver
42-caches it and sets the result code to `DNS_R_CNAME`. Then `ns/query.c`
43-interprets this as "this is a non-CNAME query and we got a CNAME
44-rdataset, so follow the chain" (which is correct in this case; however,
45-because the CNAME rdataset is self-referential, the resolver responds
46-with SERVFAIL, which is expected). The authoritative server eventually
47-answers negatively for `cname.foo.test./CNAME`. The resolver then pulls
48-out the previously cached CNAME rdataset (obtained from the A answer,
49-even though it was self-referential, the resolver cached it) and wrongly
50-sets the result to `DNS_R_CNAME` (this is the second bug). As noted
51-above, `ns/query.c` interprets this as "this is a non-CNAME query and we
52-got a CNAME rdataset, so follow the chain". The internals here are
53-slightly more subtle: it first goes into `query_cname()` and sets the
54-CNAME rdataset in the message answer section, then restarts the query to
55-follow the CNAME. The restart retrieves the CNAME rdataset from the
56-cache directly (without going to the resolver), and this time the query
57-context result is `ISC_R_SUCCESS` (since it was found) and
58-`qctx->rdataset` points to the same CNAME again (as it is
59-self-referential), so it goes directly into the
60-`query_prepresponse()/query_respond()` flow, which attempts to add the
61-rdataset to the message answer again. However, this fails because the
62-rdataset is already in the message, and the assertion which expects that
63-operation to succeed fails (due to `qctx->rdataset` being set to `NULL`
64-when ownership of the rdataset was transferred). `DNS_R_CNAME` must only
65-be set when the qtype is something other than CNAME and the resolver has
66-obtained a CNAME that needs to be followed.
67- 
68-In both cases, the correct answer from the resolver should have been
69-`ISC_R_SUCCESS` (instead of respectively `DNS_R_DNAME` and
70-`DNS_R_CNAME`) becuase the rdataset that has been looked up was found.
71- 
72-Conflict:context adapte
73-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/773d46d58c693047a5945c8fe40512edd0ac214e
74----
75- lib/dns/resolver.c | 138 ++++++++++++++++++++-------------------------
76- 1 file changed, 61 insertions(+), 77 deletions(-)
77- 
78-diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
79-index a4be00d..32b4b6b 100644
80---- a/lib/dns/resolver.c
81-+++ b/lib/dns/resolver.c
82-@@ -677,10 +677,10 @@ fctx_minimize_qname(fetchctx_t *fctx);
83- static void
84- fctx_destroy(fetchctx_t *fctx);
85- static isc_result_t
86--ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
87-- dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl,
88-- dns_ttl_t maxttl, bool optout, bool secure,
89-- dns_rdataset_t *ardataset, isc_result_t *eresultp);
90-+ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node,
91-+ dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl,
92-+ bool optout, bool secure, dns_rdataset_t *ardataset,
93-+ isc_result_t *eresultp);
94- static void
95- validated(void *arg);
96- static void
97-@@ -5282,6 +5282,46 @@ get_and_check_signer_name(dns_name_t *signer, dns_rdataset_t *sigrdataset) {
98- return true;
99- }
100-
101-+/*
102-+ * After a (non-error) negative-cache add, 'rdataset' is bound to whatever
103-+ * rdataset the cache authoritatively holds for the queried name and type.
104-+ * Map that to the result code the fetch should report:
105-+ *
106-+ * - A negative cache entry (the one we just added, or a pre-existing one):
107-+ * DNS_R_NCACHENXDOMAIN or DNS_R_NCACHENXRRSET, depending on NXDOMAIN vs
108-+ * NODATA.
109-+ *
110-+ * - A positive rdataset that was already cached at higher trust, which
111-+ * caused our negative entry to be discarded (e.g. a CNAME or DNAME cached
112-+ * by a concurrent query): ISC_R_SUCCESS, because that cached positive
113-+ * answer is what gets returned. Note the specific case for CNAME and
114-+ * DNAME *if* the query type is not the same as the rdataset type. There
115-+ * is a chain to follow *only* if the query type doesn't ask for the CNAME
116-+ * or the DNAME.
117-+ */
118-+static isc_result_t
119-+fctx_setresult(fetchctx_t *fctx, dns_rdataset_t *rdataset) {
120-+ isc_result_t result = ISC_R_SUCCESS;
121-+
122-+ if (NEGATIVE(rdataset)) {
123-+ result = NXDOMAIN(rdataset) ? DNS_R_NCACHENXDOMAIN
124-+ : DNS_R_NCACHENXRRSET;
125-+ } else if (result == ISC_R_SUCCESS && rdataset->type != fctx->type) {
126-+ switch (rdataset->type) {
127-+ case dns_rdatatype_cname:
128-+ result = DNS_R_CNAME;
129-+ break;
130-+ case dns_rdatatype_dname:
131-+ result = DNS_R_DNAME;
132-+ break;
133-+ default:
134-+ break;
135-+ }
136-+ }
137-+
138-+ return result;
139-+}
140-+
141- /*
142- * The validator has finished.
143- */
144-@@ -5529,8 +5569,7 @@ validated(void *arg) {
145- ttl = 0;
146- }
147-
148-- result = ncache_adderesult(message, fctx->cache, node, covers,
149-- now, fctx->res->view->minncachettl,
150-+ result = ncache_adderesult(fctx, message, node, covers, now,
151- ttl, val->optout, val->secure,
152- ardataset, &eresult);
153- if (result != ISC_R_SUCCESS) {
154-@@ -5786,23 +5825,7 @@ answer_response:
155- */
156- INSIST(hresp->rdataset != NULL);
157- if (dns_rdataset_isassociated(hresp->rdataset)) {
158-- if (NEGATIVE(hresp->rdataset)) {
159-- INSIST(eresult == DNS_R_NCACHENXDOMAIN ||
160-- eresult == DNS_R_NCACHENXRRSET);
161-- } else if (eresult == ISC_R_SUCCESS &&
162-- hresp->rdataset->type != fctx->type)
163-- {
164-- switch (hresp->rdataset->type) {
165-- case dns_rdatatype_cname:
166-- eresult = DNS_R_CNAME;
167-- break;
168-- case dns_rdatatype_dname:
169-- eresult = DNS_R_DNAME;
170-- break;
171-- default:
172-- break;
173-- }
174-- }
175-+ eresult = fctx_setresult(fctx, hresp->rdataset);
176- }
177-
178- hresp->result = eresult;
179-@@ -6454,24 +6477,7 @@ cache_name(fetchctx_t *fctx, dns_name_t *name, dns_message_t *message,
180- * resp->result.
181- */
182- if (dns_rdataset_isassociated(resp->rdataset)) {
183-- if (NEGATIVE(resp->rdataset)) {
184-- INSIST(eresult ==
185-- DNS_R_NCACHENXDOMAIN ||
186-- eresult == DNS_R_NCACHENXRRSET);
187-- } else if (eresult == ISC_R_SUCCESS &&
188-- resp->rdataset->type != fctx->type)
189-- {
190-- switch (resp->rdataset->type) {
191-- case dns_rdatatype_cname:
192-- eresult = DNS_R_CNAME;
193-- break;
194-- case dns_rdatatype_dname:
195-- eresult = DNS_R_DNAME;
196-- break;
197-- default:
198-- break;
199-- }
200-- }
201-+ eresult = fctx_setresult(fctx, resp->rdataset);
202- }
203- resp->result = eresult;
204- if (adbp != NULL && *adbp != NULL) {
205-@@ -6540,12 +6546,14 @@ cache_message(fetchctx_t *fctx, dns_message_t *message,
206- * eresult.
207- */
208- static isc_result_t
209--ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
210-- dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl,
211-- dns_ttl_t maxttl, bool optout, bool secure,
212-- dns_rdataset_t *ardataset, isc_result_t *eresultp) {
213-+ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node,
214-+ dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl,
215-+ bool optout, bool secure, dns_rdataset_t *ardataset,
216-+ isc_result_t *eresultp) {
217- isc_result_t result;
218- dns_rdataset_t rdataset;
219-+ dns_db_t *cache = fctx->cache;
220-+ dns_ttl_t minttl = fctx->res->view->minncachettl;
221-
222- if (ardataset == NULL) {
223- dns_rdataset_init(&rdataset);
224-@@ -6559,39 +6567,16 @@ ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
225- result = dns_ncache_add(message, cache, node, covers, now,
226- minttl, maxttl, ardataset);
227- }
228-+
229- if (result == DNS_R_UNCHANGED || result == ISC_R_SUCCESS) {
230- /*
231-- * If the cache now contains a negative entry and we
232-- * care about whether it is DNS_R_NCACHENXDOMAIN or
233-- * DNS_R_NCACHENXRRSET then extract it.
234-+ * The cache settled successfully (DNS_R_UNCHANGED means our
235-+ * negative entry was discarded in favour of existing
236-+ * higher-trust data). Either way 'ardataset' is now bound to
237-+ * the rdataset the cache holds for this name and type; derive
238-+ * the result code from it.
239- */
240-- if (NEGATIVE(ardataset)) {
241-- /*
242-- * The cache data is a negative cache entry.
243-- */
244-- if (NXDOMAIN(ardataset)) {
245-- *eresultp = DNS_R_NCACHENXDOMAIN;
246-- } else {
247-- *eresultp = DNS_R_NCACHENXRRSET;
248-- }
249-- } else {
250-- /*
251-- * The attempt to add a negative cache entry
252-- * was rejected. Set *eresultp to reflect
253-- * the type of the dataset being returned.
254-- */
255-- switch (ardataset->type) {
256-- case dns_rdatatype_cname:
257-- *eresultp = DNS_R_CNAME;
258-- break;
259-- case dns_rdatatype_dname:
260-- *eresultp = DNS_R_DNAME;
261-- break;
262-- default:
263-- *eresultp = ISC_R_SUCCESS;
264-- break;
265-- }
266-- }
267-+ *eresultp = fctx_setresult(fctx, ardataset);
268- result = ISC_R_SUCCESS;
269- }
270- if (ardataset == &rdataset && dns_rdataset_isassociated(ardataset)) {
271-@@ -6735,8 +6720,7 @@ ncache_message(fetchctx_t *fctx, dns_message_t *message,
272- ttl = 0;
273- }
274-
275-- result = ncache_adderesult(message, fctx->cache, node, covers, now,
276-- fctx->res->view->minncachettl, ttl, false,
277-+ result = ncache_adderesult(fctx, message, node, covers, now, ttl, false,
278- false, ardataset, &eresult);
279- if (result != ISC_R_SUCCESS) {
280- goto unlock;
281---
282-2.43.0
283- 
@@ -1,288 +0,0 @@
1-From e88271f2e584010157b068cc998dd76451273562 Mon Sep 17 00:00:00 2001
2-From: Colin Vidal <colin@isc.org>
3-Date: Mon, 15 Jun 2026 11:34:08 +0200
4-Subject: [PATCH] Reproducer for #5946 (assertion in some CNAME/DNAME queries)
5- 
6-Add a system test reproducing the issue reported by #5946, which
7-is also CVE-2026-12617. There are two scenarios:
8- 
9-- A client send queries for a DNAME and A record to the resolver (ns3),
10- and the authoritative server (ans2) responds positively to the A query
11- but delay the DNAME response and respond later negatively;
12- 
13-- A client send queries for a CNAME and A record to the resolver (ns3),
14- and the authoritative server (ans2) responds positively to the A query
15- but delay the CNAME response and respond later with a self-referential
16- CNAME.
17- 
18-The test does not check the results of the queries, however, it expects
19-the resolver to correctly handle those and do not assert.
20- 
21-Conflict:no
22-Reference:https://gitlab.isc.org/isc-projects/bind9/-/commit/e88271f2e584010157b068cc998dd76451273562
23----
24- .../system/cname_dname_negcache/ans2/ans.py | 98 +++++++++++++++++++
25- .../cname_dname_negcache/ns1/bar.test.db | 5 +
26- .../cname_dname_negcache/ns1/named.conf.j2 | 24 +++++
27- .../system/cname_dname_negcache/ns1/root.db | 6 ++
28- .../system/cname_dname_negcache/ns1/test.db | 8 ++
29- .../cname_dname_negcache/ns3/named.conf.j2 | 11 +++
30- .../tests_cname_dname_negcache.py | 52 ++++++++++
31- 7 files changed, 204 insertions(+)
32- create mode 100644 bin/tests/system/cname_dname_negcache/ans2/ans.py
33- create mode 100644 bin/tests/system/cname_dname_negcache/ns1/bar.test.db
34- create mode 100644 bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
35- create mode 100644 bin/tests/system/cname_dname_negcache/ns1/root.db
36- create mode 100644 bin/tests/system/cname_dname_negcache/ns1/test.db
37- create mode 100644 bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
38- create mode 100644 bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py
39- 
40-diff --git a/bin/tests/system/cname_dname_negcache/ans2/ans.py b/bin/tests/system/cname_dname_negcache/ans2/ans.py
41-new file mode 100644
42-index 0000000000..eec5c90297
43---- /dev/null
44-+++ b/bin/tests/system/cname_dname_negcache/ans2/ans.py
45-@@ -0,0 +1,98 @@
46-+"""
47-+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
48-+
49-+SPDX-License-Identifier: MPL-2.0
50-+
51-+This Source Code Form is subject to the terms of the Mozilla Public
52-+License, v. 2.0. If a copy of the MPL was not distributed with this
53-+file, you can obtain one at https://mozilla.org/MPL/2.0/.
54-+
55-+See the COPYRIGHT file distributed with this work for additional
56-+information regarding copyright ownership.
57-+"""
58-+
59-+from collections.abc import AsyncGenerator
60-+
61-+from dns import name, rcode, rdataclass, rdatatype, rrset
62-+
63-+from isctest.asyncserver import (
64-+ AsyncDnsServer,
65-+ DnsResponseSend,
66-+ QnameQtypeHandler,
67-+ QueryContext,
68-+ StaticResponseHandler,
69-+)
70-+
71-+
72-+def build_rrset(
73-+ qname: name.Name | str,
74-+ rtype: rdatatype.RdataType,
75-+ rdata: str,
76-+ ttl: int = 300,
77-+) -> rrset.RRset:
78-+ return rrset.from_text(qname, ttl, rdataclass.IN, rtype, rdata)
79-+
80-+
81-+class FooTestNsHandler(QnameQtypeHandler, StaticResponseHandler):
82-+ qnames = ["foo.test."]
83-+ qtypes = [rdatatype.NS]
84-+ answer = [build_rrset("foo.test.", rdatatype.NS, "ns.foo.test.")]
85-+ additional = [build_rrset("ns.foo.test.", rdatatype.A, "10.53.0.2")]
86-+
87-+
88-+class DelayedDnameNegHandler(QnameQtypeHandler, StaticResponseHandler):
89-+ qnames = ["foo.test."]
90-+ qtypes = [rdatatype.DNAME]
91-+ authority = [
92-+ build_rrset(
93-+ "foo.test.",
94-+ rdatatype.SOA,
95-+ "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300",
96-+ )
97-+ ]
98-+ delay = 1
99-+
100-+
101-+class DnamePosHandler(QnameQtypeHandler, StaticResponseHandler):
102-+ qnames = ["a.foo.test."]
103-+ qtypes = [rdatatype.A]
104-+ answer = [
105-+ build_rrset("foo.test.", rdatatype.DNAME, "bar.test."),
106-+ build_rrset("a.foo.test.", rdatatype.CNAME, "a.bar.test."),
107-+ ]
108-+
109-+
110-+class CnameHandler(QnameQtypeHandler):
111-+ qnames = ["cname.foo.test."]
112-+ qtypes = [rdatatype.CNAME, rdatatype.A]
113-+ answer = [build_rrset("cname.foo.test.", rdatatype.CNAME, "cname.foo.test.")]
114-+ authority = [
115-+ build_rrset(
116-+ "cname.foo.test.",
117-+ rdatatype.SOA,
118-+ "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300",
119-+ )
120-+ ]
121-+
122-+ async def get_responses(
123-+ self, qctx: QueryContext
124-+ ) -> AsyncGenerator[DnsResponseSend, None]:
125-+ qctx.prepare_new_response(with_zone_data=False)
126-+ if qctx.qtype == rdatatype.CNAME:
127-+ qctx.response.authority.extend(self.authority)
128-+ yield DnsResponseSend(qctx.response, authoritative=True, delay=1)
129-+ else:
130-+ qctx.response.answer.extend(self.answer)
131-+ yield DnsResponseSend(qctx.response, authoritative=True)
132-+
133-+
134-+def main() -> None:
135-+ server = AsyncDnsServer(default_aa=True, default_rcode=rcode.NOERROR)
136-+ server.install_response_handlers(
137-+ FooTestNsHandler(), DelayedDnameNegHandler(), DnamePosHandler(), CnameHandler()
138-+ )
139-+ server.run()
140-+
141-+
142-+if __name__ == "__main__":
143-+ main()
144-diff --git a/bin/tests/system/cname_dname_negcache/ns1/bar.test.db b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db
145-new file mode 100644
146-index 0000000000..840b9c3a2c
147---- /dev/null
148-+++ b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db
149-@@ -0,0 +1,5 @@
150-+$TTL 300
151-+bar.test. IN SOA ns.bar.test. hostmaster.bar.test. 1 600 600 1200 600
152-+bar.test. NS ns.bar.test.
153-+ns A 10.53.0.1
154-+a A 10.0.0.1
155-diff --git a/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
156-new file mode 100644
157-index 0000000000..d72dd1181d
158---- /dev/null
159-+++ b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
160-@@ -0,0 +1,24 @@
161-+options {
162-+ query-source address @ns.ip@;
163-+ port @PORT@;
164-+ pid-file "named.pid";
165-+ listen-on { @ns.ip@; };
166-+ listen-on-v6 { none; };
167-+ recursion no;
168-+ dnssec-validation no;
169-+};
170-+
171-+zone "." {
172-+ type primary;
173-+ file "root.db";
174-+};
175-+
176-+zone "test." {
177-+ type primary;
178-+ file "test.db";
179-+};
180-+
181-+zone "bar.test." {
182-+ type primary;
183-+ file "bar.test.db";
184-+};
185-diff --git a/bin/tests/system/cname_dname_negcache/ns1/root.db b/bin/tests/system/cname_dname_negcache/ns1/root.db
186-new file mode 100644
187-index 0000000000..c456c45b9d
188---- /dev/null
189-+++ b/bin/tests/system/cname_dname_negcache/ns1/root.db
190-@@ -0,0 +1,6 @@
191-+$TTL 300
192-+. IN SOA ns. hostmaster. 1 600 600 1200 600
193-+. NS a.root-servers.nil.
194-+a.root-servers.nil. A 10.53.0.1
195-+test NS ns.test
196-+ns.test A 10.53.0.1
197-diff --git a/bin/tests/system/cname_dname_negcache/ns1/test.db b/bin/tests/system/cname_dname_negcache/ns1/test.db
198-new file mode 100644
199-index 0000000000..acb68e00f8
200---- /dev/null
201-+++ b/bin/tests/system/cname_dname_negcache/ns1/test.db
202-@@ -0,0 +1,8 @@
203-+$TTL 300
204-+test. IN SOA ns.test. hostmaster.test. 1 600 600 1200 600
205-+test. NS ns.test.
206-+ns A 10.53.0.1
207-+bar NS ns.bar
208-+ns.bar A 10.53.0.1
209-+foo NS ns.foo
210-+ns.foo A 10.53.0.2
211-diff --git a/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
212-new file mode 100644
213-index 0000000000..197d72756b
214---- /dev/null
215-+++ b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
216-@@ -0,0 +1,11 @@
217-+options {
218-+ query-source address @ns.ip@;
219-+ port @PORT@;
220-+ pid-file "named.pid";
221-+ listen-on { @ns.ip@; };
222-+ listen-on-v6 { none; };
223-+ recursion yes;
224-+ dnssec-validation no;
225-+};
226-+
227-+{% include "_common/root.hint.conf" %}
228-diff --git a/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py b/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py
229-new file mode 100644
230-index 0000000000..65c44402fb
231---- /dev/null
232-+++ b/bin/tests/system/cname_dname_negcache/tests_cname_dname_negcache.py
233-@@ -0,0 +1,52 @@
234-+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
235-+#
236-+# SPDX-License-Identifier: MPL-2.0
237-+#
238-+# This Source Code Form is subject to the terms of the Mozilla Public
239-+# License, v. 2.0. If a copy of the MPL was not distributed with this
240-+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
241-+#
242-+# See the COPYRIGHT file distributed with this work for additional
243-+# information regarding copyright ownership.
244-+
245-+from re import compile as Re
246-+from socket import AF_INET, SOCK_DGRAM, socket
247-+
248-+import isctest
249-+
250-+
251-+def run_attack(ns, name1, type1, name2, type2):
252-+ msg1 = isctest.query.create(name1, type1, cd=True)
253-+ msg2 = isctest.query.create(name2, type2, cd=True)
254-+ port = int(isctest.vars.ALL["PORT"])
255-+
256-+ with socket(AF_INET, SOCK_DGRAM) as sock:
257-+ # The order the request does out doesn't matter. What is important is
258-+ # the first query starts recursion before the second query returns the
259-+ # answer, and the second query returns the answer before the first
260-+ # query returns the answer. (So, when the NOERROR/NODATA cames back
261-+ # from the first query, the cache is queried and we get the positive
262-+ # response cached from the second query attached to the fresp rdataset
263-+ # of the response of the first query.)
264-+ # Therefore, the logic is really baked into ans2, which has a 3 seconds
265-+ # delay to answer the first query.
266-+ sock.sendto(msg1.to_wire(), (ns.ip, port))
267-+ sock.sendto(msg2.to_wire(), (ns.ip, port))
268-+
269-+ # The second query come back immediately, the resolver caches the DNAME.
270-+ # The first query come back after 3s (because of intentional ans2 latency
271-+ # on foo.test./DNAME answer) and should not crash the server.
272-+ with ns.watch_log_from_start(timeout=15) as watcher:
273-+ watcher.wait_for_sequence(
274-+ [
275-+ Re(r"foo\.test\..*IN\s+SOA\s+ns\.test\.\s+op\.ns\.test\."),
276-+ ]
277-+ )
278-+
279-+
280-+def test_dname_negcache(ns3):
281-+ run_attack(ns3, "foo.test.", "DNAME", "a.foo.test.", "A")
282-+
283-+
284-+def test_cname_negcache(ns3):
285-+ run_attack(ns3, "cname.foo.test.", "CNAME", "cname.foo.test.", "A")
286---
287-2.43.0
288- 
Rbind-9.20.21.tar.xz→bind-9.20.29.tar.xz+0-0
Mbind.spec+7-36