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", 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); }