[9.18] fix: usr: Adding NSEC3 opt-out records could leave invalid records in chain

When creating an NSEC3 opt-out chain, a node in the chain could be removed too soon, causing the previous NSEC3 being unable to be found, resulting in invalid NSEC3 records to be left in the zone. This has been fixed.

Closes #5671

Backport of MR !11328

Merge branch 'backport-5671-fix-dbiterator-prev-9.18' into 'bind-9.18'

See merge request isc-projects/bind9!11341
This commit is contained in:
Ondřej Surý 2025-12-08 11:07:34 +01:00
commit 335be0e079
5 changed files with 190 additions and 2 deletions

View file

@ -0,0 +1,41 @@
/*
* 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 {
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
listen-on-v6 { none; };
allow-transfer { any; };
recursion no;
dnssec-validation no;
ixfr-from-differences yes;
sig-signing-nodes 900;
sig-signing-signatures 900;
};
include "controls.conf";
dnssec-policy "optout" {
keys {
csk lifetime unlimited algorithm ecdsa256;
};
nsec3param iterations 0 optout yes salt-length 0;
};
zone "test" {
type primary;
file "test.db";
dnssec-policy "optout";
inline-signing yes;
};

View file

@ -0,0 +1,22 @@
; 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 3600
@ IN SOA ns2.test. hostmaster.test. 1 7200 3600 24796800 3600
IN NS ns2
ns2 IN A 10.53.0.2
a IN A 127.0.0.1
$GENERATE 1-50000 child$ IN NS ns.example.
child303 IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2

View file

@ -0,0 +1,16 @@
#!/bin/sh -e
# 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.
. ../conf.sh
copy_setports ../_common/controls.conf.in ns2/controls.conf

View file

@ -0,0 +1,108 @@
#!/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 os
import re
import sys
import isctest
import pytest
pytest.importorskip("dns", minversion="2.0.0")
import dns.exception
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype
pytestmark = [
pytest.mark.skipif(
sys.version_info < (3, 7), reason="Python >= 3.7 required [GL #3001]"
),
pytest.mark.extra_artifacts(
[
"*.out",
"ns2/*.infile",
"ns2/*.signed",
"ns2/*.jnl",
"ns2/*.jbk",
"ns2/controls.conf",
"ns2/dsset-*",
"ns2/K*",
]
),
]
def has_nsec3param(zone, response):
match = rf"{re.escape(zone)}\.\s+\d+\s+IN\s+NSEC3PARAM\s+1\s+0\s+0\s+-"
for rr in response.answer:
if re.search(match, rr.to_text()):
return True
return False
def do_query(server, qname, qtype, tcp=False):
msg = isctest.query.create(qname, qtype)
query_func = isctest.query.tcp if tcp else isctest.query.udp
response = query_func(msg, server.ip, expected_rcode=dns.rcode.NOERROR)
return response
def do_xfr(server, qname):
xfr = dns.zone.Zone(origin=f"{qname}.", relativize=False)
dns.query.inbound_xfr(
where=server.ip, txn_manager=xfr, port=int(os.environ["PORT"])
)
return xfr
def verify_zone(zone, transfer):
verify = os.getenv("VERIFY")
assert verify is not None
filename = f"{zone}.out"
with open(filename, "w", encoding="utf-8") as file:
file.write(transfer.to_text())
# dnssec-verify command with default arguments.
verify_cmd = [verify, "-z", "-o", zone, filename]
verifier = isctest.run.cmd(verify_cmd)
if verifier.returncode != 0:
isctest.log.error(f"dnssec-verify {zone}. failed")
return verifier.returncode == 0
def test_optout(ns2):
zone = "test"
# Wait until the provided zone is signed and then verify its DNSSEC data.
def check_nsec3param():
response = do_query(ns2, zone, "NSEC3PARAM")
return has_nsec3param(zone, response)
# check zone is fully signed.
isctest.run.retry_with_timeout(check_nsec3param, timeout=300)
# check if zone if DNSSEC valid.
transfer = do_xfr(ns2, zone)
assert verify_zone(zone, transfer)

View file

@ -9582,11 +9582,12 @@ dbiterator_prev(dns_dbiterator_t *iterator) {
resume_iteration(rbtdbiter);
}
dereference_iter_node(rbtdbiter);
name = dns_fixedname_name(&rbtdbiter->name);
origin = dns_fixedname_name(&rbtdbiter->origin);
result = dns_rbtnodechain_prev(rbtdbiter->current, name, origin);
dereference_iter_node(rbtdbiter);
if (rbtdbiter->current == &rbtdbiter->nsec3chain &&
(result == ISC_R_SUCCESS || result == DNS_R_NEWORIGIN))
{