From a338e254beda3085fbe7b91905e668ba9b0cfb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 20 Feb 2026 16:35:29 +0100 Subject: [PATCH 1/2] Test excessive RRSIG(NSEC) in signed zones Trigger a memory leak by adding extra RRSIG(NSEC) to a signed zone which exceeds the resolver's configured max-records-per-type limit. --- bin/tests/system/nsec/ns1/named.conf.j2 | 29 +++++++ bin/tests/system/nsec/ns1/root.db | 24 +++++ .../nsec/ns2/excessive-nsec-rrsigs.db.in | 24 +++++ bin/tests/system/nsec/ns2/named.conf.j2 | 29 +++++++ bin/tests/system/nsec/ns3/named.conf.j2 | 35 ++++++++ bin/tests/system/nsec/ns3/trusted.conf.j2 | 1 + .../system/nsec/tests_excessive_rrsigs.py | 87 +++++++++++++++++++ 7 files changed, 229 insertions(+) create mode 100644 bin/tests/system/nsec/ns1/named.conf.j2 create mode 100644 bin/tests/system/nsec/ns1/root.db create mode 100644 bin/tests/system/nsec/ns2/excessive-nsec-rrsigs.db.in create mode 100644 bin/tests/system/nsec/ns2/named.conf.j2 create mode 100644 bin/tests/system/nsec/ns3/named.conf.j2 create mode 120000 bin/tests/system/nsec/ns3/trusted.conf.j2 create mode 100755 bin/tests/system/nsec/tests_excessive_rrsigs.py diff --git a/bin/tests/system/nsec/ns1/named.conf.j2 b/bin/tests/system/nsec/ns1/named.conf.j2 new file mode 100644 index 0000000000..eb079c95ab --- /dev/null +++ b/bin/tests/system/nsec/ns1/named.conf.j2 @@ -0,0 +1,29 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion no; + notify yes; +}; + +zone "." { + type primary; + file "root.db"; +}; diff --git a/bin/tests/system/nsec/ns1/root.db b/bin/tests/system/nsec/ns1/root.db new file mode 100644 index 0000000000..49f0d671cb --- /dev/null +++ b/bin/tests/system/nsec/ns1/root.db @@ -0,0 +1,24 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 300 +. IN SOA . a.root.servers.nil. ( + 2000042100 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +excessive-nsec-rrsigs. NS ns2.excessive-nsec-rrsigs. +ns2.excessive-nsec-rrsigs. A 10.53.0.2 diff --git a/bin/tests/system/nsec/ns2/excessive-nsec-rrsigs.db.in b/bin/tests/system/nsec/ns2/excessive-nsec-rrsigs.db.in new file mode 100644 index 0000000000..135e93d287 --- /dev/null +++ b/bin/tests/system/nsec/ns2/excessive-nsec-rrsigs.db.in @@ -0,0 +1,24 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 300 +@ IN SOA mname1. . ( + 1 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +@ NS ns2 +ns2 A 10.53.0.2 + +* A 127.0.0.1 diff --git a/bin/tests/system/nsec/ns2/named.conf.j2 b/bin/tests/system/nsec/ns2/named.conf.j2 new file mode 100644 index 0000000000..32a566ad79 --- /dev/null +++ b/bin/tests/system/nsec/ns2/named.conf.j2 @@ -0,0 +1,29 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +options { + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; + recursion no; + notify yes; +}; + +zone "excessive-nsec-rrsigs" { + type primary; + file "excessive-nsec-rrsigs.db.signed"; +}; diff --git a/bin/tests/system/nsec/ns3/named.conf.j2 b/bin/tests/system/nsec/ns3/named.conf.j2 new file mode 100644 index 0000000000..5a8ef09276 --- /dev/null +++ b/bin/tests/system/nsec/ns3/named.conf.j2 @@ -0,0 +1,35 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +// validating resolver + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation yes; + + max-records-per-type 2; +}; + +zone "." { + type hint; + file "../../_common/root.hint"; +}; + +include "trusted.conf"; diff --git a/bin/tests/system/nsec/ns3/trusted.conf.j2 b/bin/tests/system/nsec/ns3/trusted.conf.j2 new file mode 120000 index 0000000000..cb0be77b22 --- /dev/null +++ b/bin/tests/system/nsec/ns3/trusted.conf.j2 @@ -0,0 +1 @@ +../../_common/trusted.conf.j2 \ No newline at end of file diff --git a/bin/tests/system/nsec/tests_excessive_rrsigs.py b/bin/tests/system/nsec/tests_excessive_rrsigs.py new file mode 100755 index 0000000000..8bc62fda71 --- /dev/null +++ b/bin/tests/system/nsec/tests_excessive_rrsigs.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.RRSIG +import dns.zone + +from isctest.run import EnvCmd + +import isctest + + +def duplicate_rrsig(rdata, i): + return dns.rdtypes.ANY.RRSIG.RRSIG( + rdclass=rdata.rdclass, + rdtype=rdata.rdtype, + type_covered=rdata.type_covered, + algorithm=rdata.algorithm, + labels=rdata.labels, + # increment orig TTL so the rdataset isn't treated as identical record by dnspython + original_ttl=rdata.original_ttl + i, + expiration=rdata.expiration, + inception=rdata.inception, + key_tag=rdata.key_tag, + signer=rdata.signer, + signature=rdata.signature, + ) + + +def bootstrap(): + keygen = EnvCmd("KEYGEN", "-a ECDSA256 -Kns2 -q") + signer = EnvCmd("SIGNER", "-S -g") + + zone = "excessive-nsec-rrsigs" + infile = f"{zone}.db.in" + signedfile = f"{zone}.db.signed" + + isctest.log.info(f"{zone}: generate ksk and zsk") + ksk_name = keygen(f"-f KSK {zone}").out.strip() + keygen(f"{zone}").out.strip() + ksk = isctest.kasp.Key(ksk_name, keydir="ns2") + + isctest.log.info(f"{zone}: sign zone") + signer(f"-P -x -O full -o {zone} -f {signedfile} {infile}", cwd="ns2") + + isctest.log.info( + f"{zone}: duplicate the RRSIG(NSEC) to exceed max-records-per-type" + ) + zone = dns.zone.from_file(f"ns2/{signedfile}", origin=zone) + for node in zone.values(): + rrsig_rdataset = node.find_rdataset( + dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NSEC + ) + orig = rrsig_rdataset[0] + rrsig_rdataset.add(duplicate_rrsig(orig, 1)) + rrsig_rdataset.add(duplicate_rrsig(orig, 2)) + zone.to_file(f"ns2/{signedfile}", sorted=True) + + return { + "trust_anchors": [ + ksk.into_ta("static-key"), + ], + } + + +# reproducer for CVE-2026-3104 [GL#5742] +def test_excessive_rrsigs(ns3): + # the real test is that there is no crash on shutdown - checked by the test + # framework when the test finishes + + # multiple queries seem more reliable to reproduce the memory leak, using a + # single query sometimes didn't cause a crash on shutdown + for i in range(10): + msg = isctest.query.create(f"x{i}.excessive-nsec-rrsigs", "A") + res = isctest.query.udp(msg, ns3.ip, attempts=1) + isctest.check.servfail(res) From a854a5c83d3a8556d31df880d22e3f835527d45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 10 Feb 2026 16:16:25 +0100 Subject: [PATCH 2/2] Fix memory leak in QPcache addnoqname/addclosest mechanism The attacker that controls DNSSEC-signed zone can trigger a memory leak in the addnoqname() and/or addclosest() by creating more than max-records-per-type RRSIG for any NSEC records. The memory leaks have been fixed. --- lib/dns/qpcache.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/dns/qpcache.c b/lib/dns/qpcache.c index dd16406659..3851480d40 100644 --- a/lib/dns/qpcache.c +++ b/lib/dns/qpcache.c @@ -2981,7 +2981,7 @@ addnoqname(isc_mem_t *mctx, dns_slabheader_t *newheader, uint32_t maxrrperset, dns_slabheader_proof_t *noqname = NULL; dns_name_t name = DNS_NAME_INITEMPTY; dns_rdataset_t neg = DNS_RDATASET_INIT, negsig = DNS_RDATASET_INIT; - isc_region_t r1, r2; + isc_region_t r1 = { .base = NULL }, r2 = { .base = NULL }; result = dns_rdataset_getnoqname(rdataset, &name, &neg, &negsig); RUNTIME_CHECK(result == ISC_R_SUCCESS); @@ -3001,6 +3001,14 @@ addnoqname(isc_mem_t *mctx, dns_slabheader_t *newheader, uint32_t maxrrperset, newheader->noqname = noqname; cleanup: + if (result != ISC_R_SUCCESS) { + if (r1.base != NULL) { + isc_mem_put(mctx, r1.base, r1.length); + } + if (r2.base != NULL) { + isc_mem_put(mctx, r2.base, r2.length); + } + } dns_rdataset_disassociate(&neg); dns_rdataset_disassociate(&negsig); @@ -3014,7 +3022,7 @@ addclosest(isc_mem_t *mctx, dns_slabheader_t *newheader, uint32_t maxrrperset, dns_slabheader_proof_t *closest = NULL; dns_name_t name = DNS_NAME_INITEMPTY; dns_rdataset_t neg = DNS_RDATASET_INIT, negsig = DNS_RDATASET_INIT; - isc_region_t r1, r2; + isc_region_t r1 = { .base = NULL }, r2 = { .base = NULL }; result = dns_rdataset_getclosest(rdataset, &name, &neg, &negsig); RUNTIME_CHECK(result == ISC_R_SUCCESS); @@ -3034,6 +3042,14 @@ addclosest(isc_mem_t *mctx, dns_slabheader_t *newheader, uint32_t maxrrperset, newheader->closest = closest; cleanup: + if (result != ISC_R_SUCCESS) { + if (r1.base != NULL) { + isc_mem_put(mctx, r1.base, r1.length); + } + if (r2.base != NULL) { + isc_mem_put(mctx, r2.base, r2.length); + } + } dns_rdataset_disassociate(&neg); dns_rdataset_disassociate(&negsig); return result; @@ -3106,12 +3122,12 @@ qpcache_addrdataset(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version, DNS_SLABHEADER_SETATTR(newheader, DNS_SLABHEADERATTR_OPTOUT); } if (rdataset->attributes.noqname) { - RETERR(addnoqname(qpnode->mctx, newheader, qpdb->maxrrperset, - rdataset)); + CHECK(addnoqname(qpnode->mctx, newheader, qpdb->maxrrperset, + rdataset)); } if (rdataset->attributes.closest) { - RETERR(addclosest(qpnode->mctx, newheader, qpdb->maxrrperset, - rdataset)); + CHECK(addclosest(qpnode->mctx, newheader, qpdb->maxrrperset, + rdataset)); } nlock = &qpdb->buckets[qpnode->locknum].lock; @@ -3179,6 +3195,9 @@ qpcache_addrdataset(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version, INSIST(tlocktype == isc_rwlocktype_none); + return result; +cleanup: + dns_slabheader_destroy(&newheader); return result; }