[9.20] fix: usr: Fix a crash when querying an empty non-terminal in a wildcard zone in RBTDB

A query for an empty non-terminal in a wildcard zone served
from the RBT zone database could abort named with an assertion
failure.  It now returns the correct NODATA answer.

Closes #6170

Merge branch '6170-fix-non-terminal-wildcard-in-rbtdb-9.20' into 'bind-9.20'

See merge request isc-projects/bind9!12299
This commit is contained in:
Ondřej Surý 2026-06-22 22:46:35 +02:00
commit b8e5801ec5
7 changed files with 142 additions and 1 deletions

View file

@ -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 .

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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",

View file

@ -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",

View file

@ -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);
}