From a7a77a0db5612f97f17d1532c1411c7be0125204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Mon, 22 Jun 2026 21:49:04 +0200 Subject: [PATCH 1/2] Handle DNS_R_NEWORIGIN when stepping to a wildcard's successor dns_rbtnodechain_next() returns DNS_R_NEWORIGIN rather than ISC_R_SUCCESS whenever the successor lies in a different RBT origin, which is the case when the queried name's predecessor is an empty non-terminal with children. Treating that as a failure made wildcard_blocked() miss the empty non-terminal that must block the wildcard, so the expansion proceeded and later tripped an assertion. activeempty() already handled both results. --- lib/dns/rbt-zonedb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dns/rbt-zonedb.c b/lib/dns/rbt-zonedb.c index 98c1d6ba18..b405cf845f 100644 --- a/lib/dns/rbt-zonedb.c +++ b/lib/dns/rbt-zonedb.c @@ -429,7 +429,7 @@ wildcard_blocked(rbtdb_search_t *search, const dns_name_t *qname, /* Now reset the chain and look for a successor with data. */ chain = search->chain; result = dns_rbtnodechain_next(&chain, NULL, NULL); - if (result == ISC_R_SUCCESS) { + if (result == ISC_R_SUCCESS || result == DNS_R_NEWORIGIN) { check_next = step(search, &chain, FORWARD, next); } From 033a15061be3592093d0788b9ab3c98a4e0a3775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Mon, 22 Jun 2026 21:49:16 +0200 Subject: [PATCH 2/2] Add a regression test for an empty non-terminal under a wildcard The NSEC3-signed entwild.test zone places an apex wildcard above empty non-terminals; querying one of them with DNSSEC requested reproduces the abort seen with the RBT zone database. The query must instead return NODATA with an NSEC3 proof. The zone uses the default zone database, so it only exercises RBTDB on a build that defaults to it. Assisted-by: Claude:claude-opus-4-8 --- bin/tests/system/wildcard/ns1/entwild.db.in | 26 ++++++ bin/tests/system/wildcard/ns1/named.conf.j2 | 10 +++ bin/tests/system/wildcard/ns1/sign.sh | 14 +++ .../wildcard/tests_nonterminal_wildcard.py | 89 +++++++++++++++++++ .../system/wildcard/tests_sh_wildcard.py | 1 + bin/tests/system/wildcard/tests_wildcard.py | 1 + 6 files changed, 141 insertions(+) create mode 100644 bin/tests/system/wildcard/ns1/entwild.db.in create mode 100644 bin/tests/system/wildcard/tests_nonterminal_wildcard.py diff --git a/bin/tests/system/wildcard/ns1/entwild.db.in b/bin/tests/system/wildcard/ns1/entwild.db.in new file mode 100644 index 0000000000..aea203bbea --- /dev/null +++ b/bin/tests/system/wildcard/ns1/entwild.db.in @@ -0,0 +1,26 @@ +; 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. + +; GL #6170 regression zone. +; +; An apex wildcard combined with a deeper name (en.jun2026.2048._domainkey) +; that forms empty non-terminals. Querying one of those empty non-terminals +; (e.g. 2048._domainkey) with DNSSEC requested, while the zone is served from +; the RBT zone database, used to abort named in wildcard_blocked() because the +; DNS_R_NEWORIGIN result of dns_rbtnodechain_next() was not handled. + +$TTL 3600 +@ IN SOA ns hostmaster ( 1 3600 1200 604800 3600 ) + IN NS ns +ns IN A 10.53.0.1 +* IN A 192.0.2.1 +en.jun2026.2048._domainkey IN TXT "v=DKIM1; k=rsa; p=PUBLICKEYDATA" +crm._domainkey IN CNAME . diff --git a/bin/tests/system/wildcard/ns1/named.conf.j2 b/bin/tests/system/wildcard/ns1/named.conf.j2 index 7ae63ac1b9..eb781e9222 100644 --- a/bin/tests/system/wildcard/ns1/named.conf.j2 +++ b/bin/tests/system/wildcard/ns1/named.conf.j2 @@ -40,6 +40,16 @@ zone "nestedwild.test" { check-names ignore; }; +/* + * GL #6170: querying an empty non-terminal in this NSEC3-signed zone, which + * also has a wildcard, used to abort named when served from RBTDB. + */ +zone "entwild.test" { + type primary; + file "entwild.db.signed"; + check-names ignore; +}; + /* * The contents of nsec3 and private.nsec3 are specially chosen to * have separate NSEC3 records for the "no qname proof" and the diff --git a/bin/tests/system/wildcard/ns1/sign.sh b/bin/tests/system/wildcard/ns1/sign.sh index cc01160757..c537167bd5 100755 --- a/bin/tests/system/wildcard/ns1/sign.sh +++ b/bin/tests/system/wildcard/ns1/sign.sh @@ -20,6 +20,20 @@ cp allwild.db.in allwild.db cp example.db.in example.db cp nestedwild.db.in nestedwild.db +# GL #6170: NSEC3-signed zone with a wildcard above empty non-terminals. +zone=entwild.test +infile=entwild.db.in +zonefile=entwild.db +outfile=entwild.db.signed + +keyname1=$($KEYGEN -a ${DEFAULT_ALGORITHM} -n zone $zone 2>/dev/null) +keyname2=$($KEYGEN -f KSK -a ${DEFAULT_ALGORITHM} -n zone $zone 2>/dev/null) + +cat $infile $keyname1.key $keyname2.key >$zonefile + +$SIGNER -3 - -H 10 -o $zone -f $outfile $zonefile >/dev/null 2>signer.err || cat signer.err +echo_i "signed $zone" + zone=nsec infile=nsec.db.in zonefile=nsec.db diff --git a/bin/tests/system/wildcard/tests_nonterminal_wildcard.py b/bin/tests/system/wildcard/tests_nonterminal_wildcard.py new file mode 100644 index 0000000000..ee7bbbfa7c --- /dev/null +++ b/bin/tests/system/wildcard/tests_nonterminal_wildcard.py @@ -0,0 +1,89 @@ +# 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. + +""" +Regression test for GL #6170. + +The NSEC3-signed zone entwild.test has an apex wildcard together with a deeper +name (en.jun2026.2048._domainkey) that forms empty non-terminals. Querying +one of those empty non-terminals (2048._domainkey) with DNSSEC requested, while +the zone is served from the RBT zone database, used to abort named with: + + lib/dns/include/dns/name.h:1013: REQUIRE(suffixlabels <= name->labels) failed + +wildcard_blocked() in lib/dns/rbt-zonedb.c did not treat the DNS_R_NEWORIGIN +result of dns_rbtnodechain_next() as a successful step when looking for the +successor of the queried name, so the empty non-terminal that blocks the +wildcard expansion was missed. The empty non-terminal must instead be answered +with NODATA and an NSEC3 proof. + +The bug only affected RBTDB; QPzone is unaffected. +""" + +import dns.rdatatype +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "ns1/K*", + "ns1/dsset-*", + "ns1/*.signed", + "ns1/allwild.db", + "ns1/entwild.db", + "ns1/example.db", + "ns1/nestedwild.db", + "ns1/nsec.db", + "ns1/nsec3.db", + "ns1/private.nsec.conf", + "ns1/private.nsec.db", + "ns1/private.nsec3.conf", + "ns1/private.nsec3.db", + "ns1/root.db", + "ns1/signer.err", + "ns1/trusted.conf", + ] +) + +IP_ADDR = "10.53.0.1" + +# 2048._domainkey.entwild.test. and _domainkey.entwild.test. are empty +# non-terminals: they exist only because en.jun2026.2048._domainkey.entwild.test. +# and crm._domainkey.entwild.test. do. They sit between the apex wildcard and +# the existing names. +ENT_NAME = "2048._domainkey.entwild.test." + + +def test_empty_nonterminal_under_wildcard_is_nodata(named_port): + """Querying the empty non-terminal must yield NODATA, not crash named.""" + query = isctest.query.create(ENT_NAME, "A") + response = isctest.query.tcp(query, IP_ADDR, named_port) + + isctest.check.is_response_to(response, query) + # The empty non-terminal exists, so the wildcard must not expand: NODATA. + isctest.check.noerror(response) + isctest.check.empty_answer(response) + # The NSEC3 empty-non-terminal proof is the code path that used to crash. + assert any( + rrset.rdtype == dns.rdatatype.NSEC3 for rrset in response.authority + ), str(response) + + +def test_wildcard_still_expands_without_blocking_ent(named_port): + """A name with no blocking empty non-terminal is still synthesized.""" + query = isctest.query.create("nonexistent.entwild.test.", "A") + response = isctest.query.tcp(query, IP_ADDR, named_port) + + isctest.check.is_response_to(response, query) + isctest.check.noerror(response) + assert response.answer, str(response) + assert response.answer[0][0].to_text() == "192.0.2.1", str(response) diff --git a/bin/tests/system/wildcard/tests_sh_wildcard.py b/bin/tests/system/wildcard/tests_sh_wildcard.py index db01be0e1e..c2a54e919b 100644 --- a/bin/tests/system/wildcard/tests_sh_wildcard.py +++ b/bin/tests/system/wildcard/tests_sh_wildcard.py @@ -18,6 +18,7 @@ pytestmark = pytest.mark.extra_artifacts( "ns1/dsset-*", "ns1/*.signed", "ns1/allwild.db", + "ns1/entwild.db", "ns1/example.db", "ns1/nestedwild.db", "ns1/nsec.db", diff --git a/bin/tests/system/wildcard/tests_wildcard.py b/bin/tests/system/wildcard/tests_wildcard.py index 3ea3fdc7d3..184d888cde 100755 --- a/bin/tests/system/wildcard/tests_wildcard.py +++ b/bin/tests/system/wildcard/tests_wildcard.py @@ -51,6 +51,7 @@ pytestmark = pytest.mark.extra_artifacts( "ns1/dsset-*", "ns1/*.signed", "ns1/allwild.db", + "ns1/entwild.db", "ns1/example.db", "ns1/nestedwild.db", "ns1/nsec.db",