From 95fc625d6052386233f7cc339c6317a1945a24ad Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 3 Mar 2026 14:00:38 -0800 Subject: [PATCH 01/30] Disable recursion for non-IN classes Force recursion off, and set allow-recursion/allow-recursion-on ACLs to none, for views with a class other than IN. Log a configuration warning if recursion is explicitly enabled for a non-IN view. This addresses YWH-PGM40640-74 and YWH-PGM40640-75 by preventing any attempt at recursive processing in a class-CHAOS view, ensuring that server addresses used for recursive queries and received in recursive responses are of the expected format. Fixes: isc-projects/bind9#5780 Fixes: isc-projects/bind9#5781 (cherry picked from commit 7becff1a14684a68208c92b3b0315c045c05ad75) --- bin/named/server.c | 41 +++++++-------------------- bin/tests/system/allow_query/tests.sh | 2 +- bin/tests/system/checkconf/tests.sh | 1 + bin/tests/system/resolver/tests.sh | 8 ++++-- lib/isccfg/check.c | 22 ++++++++++++-- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index df9acea28f..5d0bbfaa30 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4428,7 +4428,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, obj = NULL; result = named_config_get(maps, "recursion", &obj); INSIST(result == ISC_R_SUCCESS); - view->recursion = cfg_obj_asboolean(obj); + view->recursion = (view->rdclass == dns_rdataclass_in && + cfg_obj_asboolean(obj)); if (named_g_maxcachesize != 0) { /* @@ -5144,35 +5145,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, } /* - * We have default hints for class IN if we need them. + * We have default root hints for class IN if we need them. + * Each view gets its own rootdb so a priming response only + * writes into that view's copy. Other classes don't support + * recursion and don't need hints. */ if (view->rdclass == dns_rdataclass_in && view->hints == NULL) { dns_view_sethints(view, named_g_server->in_roothints); } - /* - * If we still have no hints, this is a non-IN view with no - * "hints zone" configured. Issue a warning, except if this - * is a root server. Root servers never need to consult - * their hints, so it's no point requiring users to configure - * them. - */ - if (view->hints == NULL) { - dns_zone_t *rootzone = NULL; - (void)dns_view_findzone(view, dns_rootname, DNS_ZTFIND_EXACT, - &rootzone); - if (rootzone != NULL) { - dns_zone_detach(&rootzone); - need_hints = false; - } - if (need_hints) { - isc_log_write(named_g_lctx, NAMED_LOGCATEGORY_GENERAL, - NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, - "no root hints for view '%s'", - view->name); - } - } - /* * Configure the view's transports (DoT/DoH) */ @@ -5408,14 +5389,14 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, "allow-proxy-on", NULL, actx, named_g_mctx, &view->proxyonacl)); - if (strcmp(view->name, "_bind") != 0 && - view->rdclass != dns_rdataclass_chaos) - { - /* named.conf only */ + if (view->rdclass != dns_rdataclass_in) { + view->recursion = false; + dns_acl_none(named_g_mctx, &view->recursionacl); + dns_acl_none(named_g_mctx, &view->recursiononacl); + } else { CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion", NULL, actx, named_g_mctx, &view->recursionacl)); - /* named.conf only */ CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion-on", NULL, actx, named_g_mctx, &view->recursiononacl)); diff --git a/bin/tests/system/allow_query/tests.sh b/bin/tests/system/allow_query/tests.sh index b4c5aae555..be609a9f57 100644 --- a/bin/tests/system/allow_query/tests.sh +++ b/bin/tests/system/allow_query/tests.sh @@ -703,7 +703,7 @@ $DIG -p ${PORT} @10.53.1.2 d.normal.example a >dig.out.ns3.4.$n || ret=1 grep 'recursion requested but not available' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'status: REFUSED' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'EDE: 18 (Prohibited)' dig.out.ns3.4.$n >/dev/null || ret=1 -nextpart ns3/named.run | grep 'allow-recursion-on did not match' >/dev/null || ret=1 +nextpart ns3/named.run | grep 'allow-query-cache-on did not match' >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 3d4fd83cf4..2d7bb308c5 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -545,6 +545,7 @@ $CHECKCONF -l good.conf \ | grep -v "is not implemented" \ | grep -v "is not recommended" \ | grep -v "no longer exists" \ + | grep -v "recursion will be disabled" \ | grep -v "is obsolete" >checkconf.out$n || ret=1 diff good.zonelist checkconf.out$n >diff.out$n || ret=1 if [ $ret -ne 0 ]; then diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh index 485d9d500e..1eec877a5a 100755 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -782,10 +782,12 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) -echo_i "checking NXDOMAIN is returned when querying non existing domain in CH class ($n)" +echo_i "checking REFUSED is returned when querying non existing domain in CH class ($n)" ret=0 -dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.${n} || ret=1 -grep "status: NXDOMAIN" dig.ns1.out.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 hostname.chaostest txt ch >dig.ns1.out.1.${n} || ret=1 +grep "status: NOERROR" dig.ns1.out.1.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.2.${n} || ret=1 +grep "status: REFUSED" dig.ns1.out.2.${n} >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 35be50fd60..e3e27590d0 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -3041,13 +3041,17 @@ check_mirror_zone_notify(const cfg_obj_t *zoptions, const char *znamestr, */ static bool check_recursion(const cfg_obj_t *config, const cfg_obj_t *voptions, - const cfg_obj_t *goptions, isc_log_t *logctx, - cfg_aclconfctx_t *actx, isc_mem_t *mctx) { + dns_rdataclass_t vclass, const cfg_obj_t *goptions, + isc_log_t *logctx, cfg_aclconfctx_t *actx, isc_mem_t *mctx) { dns_acl_t *acl = NULL; const cfg_obj_t *obj; isc_result_t result; bool retval = true; + if (vclass != dns_rdataclass_in) { + return false; + } + /* * Check the "recursion" option first. */ @@ -3905,7 +3909,8 @@ check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions, * contradicts the purpose of the former. */ if (ztype == CFG_ZONE_MIRROR && - !check_recursion(config, voptions, goptions, logctx, actx, mctx)) + !check_recursion(config, voptions, zclass, goptions, logctx, actx, + mctx)) { cfg_obj_log(zoptions, logctx, ISC_LOG_ERROR, "zone '%s': mirror zones cannot be used if " @@ -5719,6 +5724,17 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, cfg_aclconfctx_create(mctx, &actx); + if (vclass != dns_rdataclass_in) { + if (check_recursion(config, voptions, dns_rdataclass_in, + options, logctx, actx, mctx)) + { + cfg_obj_log(opts, logctx, ISC_LOG_WARNING, + "recursion will be disabled for " + "non-IN view '%s'", + viewname); + } + } + if (voptions != NULL) { (void)cfg_map_get(voptions, "zone", &zones); } else { From 134706912f273c04a2e785e4a03b1fa8fa278f77 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Wed, 4 Mar 2026 13:24:52 -0800 Subject: [PATCH 02/30] Disable UPDATE and NOTIFY for non-IN classes Return NOTIMP for UPDATE and NOTIFY requests received for views with a class other than IN. Only QUERY is now supported for non-IN views such as CHAOS. When running dns dns_rdata_tostruct() with types that are only defined for class IN, ensure that the class is correct before proceeding. Add an assertion that any zone being updated is of class IN. (Note that previously, a DLZ zone could have its class value set incorrectly to NONE; this has been fixed.) This addresses YWH-PGM40640-70 and YWH-PGM40640-73 (as well as any similar problems that might have occurred in the future) by minimizing the code paths that can be reached by rdata classes other than IN, so it is safe for the implementation to assume that rdatatypes that are only defined for class IN, such as SVCB or WKS, have been parsed and validated, and not accepted as unknown/opaque data. Fixes: isc-projects/bind9#5777 Fixes: isc-projects/bind9#5779 (cherry picked from commit a6d8e330ed6cf0021bff3f00aa1dc7a296f5aec0) --- bin/named/server.c | 2 ++ lib/dns/adb.c | 5 +++-- lib/ns/client.c | 8 ++++++++ lib/ns/update.c | 43 ++++++++++++++++++++++++------------------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index 5d0bbfaa30..63bb311d3a 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1917,10 +1917,12 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) { dns_rdataclass_t zclass = view->rdclass; isc_result_t result; + dns_zone_setclass(zone, zclass); result = dns_zonemgr_managezone(named_g_server->zonemgr, zone); if (result != ISC_R_SUCCESS) { return result; } + dns_zone_setstats(zone, named_g_server->zonestats); return named_zone_configure_writeable_dlz(dlzdb, zone, zclass, origin); diff --git a/lib/dns/adb.c b/lib/dns/adb.c index 1514fb46b9..7bfafff6d4 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -567,6 +567,9 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdtype = rdataset->type; + REQUIRE(rdataset->rdclass == dns_rdataclass_in); + REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa); + switch (rdataset->trust) { case dns_trust_glue: case dns_trust_additional: @@ -579,8 +582,6 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdataset->ttl = ttlclamp(rdataset->ttl); } - REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa); - for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; result = dns_rdataset_next(rdataset)) { diff --git a/lib/ns/client.c b/lib/ns/client.c index 9bae43b380..32f0240180 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -2413,6 +2413,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_update: CTRACE("update"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } #ifdef HAVE_DNSTAP dns_dt_send(client->view, DNS_DTTYPE_UQ, &client->peeraddr, &client->destsockaddr, transport_type, NULL, @@ -2423,6 +2427,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_notify: CTRACE("notify"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } ns_client_settimeout(client, 60); ns_notify_start(client, client->handle); break; diff --git a/lib/ns/update.c b/lib/ns/update.c index 031efef0a1..dbd5320367 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -997,7 +997,9 @@ ssu_checkrr(void *data, rr_t *rr) { RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &ptr.ptr; } - if (rr->rdata.type == dns_rdatatype_srv) { + if (rr->rdata.rdclass == dns_rdataclass_in && + rr->rdata.type == dns_rdatatype_srv) + { result = dns_rdata_tostruct(&rr->rdata, &srv, NULL); RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &srv.target; @@ -1352,7 +1354,10 @@ replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { return true; } } - if (db_rr->type == dns_rdatatype_wks) { + + if (db_rr->rdclass == dns_rdataclass_in && + db_rr->type == dns_rdatatype_wks) + { /* * Compare the address and protocol fields only. These * form the first five bytes of the RR data. Do a @@ -1495,8 +1500,7 @@ cleanup: * 'rdata', and 'ttl', respectively. */ static void -get_current_rr(dns_message_t *msg, dns_section_t section, - dns_rdataclass_t zoneclass, dns_name_t **name, +get_current_rr(dns_message_t *msg, dns_section_t section, dns_name_t **name, dns_rdata_t *rdata, dns_rdatatype_t *covers, dns_ttl_t *ttl, dns_rdataclass_t *update_class) { dns_rdataset_t *rdataset; @@ -1512,7 +1516,7 @@ get_current_rr(dns_message_t *msg, dns_section_t section, dns_rdataset_current(rdataset, rdata); INSIST(dns_rdataset_next(rdataset) == ISC_R_NOMORE); *update_class = rdata->rdclass; - rdata->rdclass = zoneclass; + rdata->rdclass = dns_rdataclass_in; } /*% @@ -1612,7 +1616,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_message_t *request = client->message; isc_mem_t *mctx = client->manager->mctx; dns_aclenv_t *env = client->manager->aclenv; - dns_rdataclass_t zoneclass; dns_rdatatype_t covers; dns_name_t *zonename = NULL; unsigned int *maxbytype = NULL; @@ -1624,11 +1627,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); dns_zone_getssutable(zone, &ssutable); options = dns_zone_getoptions(zone); dns_db_currentversion(db, &ver); + /* Updates are only supported for class IN. */ + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + /* * Update message processing can leak record existence information * so check that we are allowed to query this zone. Additionally, @@ -1678,13 +1683,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &name, &rdata, + &covers, &ttl, &update_class); if (!dns_name_issubdomain(name, zonename)) { FAILC(DNS_R_NOTZONE, "update RR is outside zone"); } - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * Check for meta-RRs. The RFC2136 pseudocode says * check for ANY|AXFR|MAILA|MAILB, but the text adds @@ -1698,6 +1703,7 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(DNS_R_REFUSED); } if ((options & DNS_ZONEOPT_CHECKSVCB) != 0 && + rdata.rdclass == dns_rdataclass_in && rdata.type == dns_rdatatype_svcb) { result = dns_rdata_checksvcb(name, &rdata); @@ -1785,7 +1791,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { } if (update_class == dns_rdataclass_any && - zoneclass == dns_rdataclass_in && (rdata.type == dns_rdatatype_ptr || rdata.type == dns_rdatatype_srv)) { @@ -2717,7 +2722,6 @@ update_action(void *arg) { isc_mem_t *mctx = client->manager->mctx; dns_rdatatype_t covers; dns_message_t *request = client->message; - dns_rdataclass_t zoneclass; dns_name_t *zonename = NULL; dns_fixedname_t tmpnamefixed; dns_name_t *tmpname = NULL; @@ -2734,9 +2738,10 @@ update_action(void *arg) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); options = dns_zone_getoptions(zone); + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + is_inline = (!dns_zone_israw(zone) && dns_zone_issecure(zone)); is_maintain = ((dns_zone_getkeyopts(zone) & DNS_ZONEKEY_MAINTAIN) != 0); is_signing = is_inline || (!is_inline && is_maintain); @@ -2761,8 +2766,8 @@ update_action(void *arg) { dns_rdataclass_t update_class; bool flag; - get_current_rr(request, DNS_SECTION_PREREQUISITE, zoneclass, - &name, &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_PREREQUISITE, &name, &rdata, + &covers, &ttl, &update_class); if (ttl != 0) { PREREQFAILC(DNS_R_FORMERR, @@ -2825,7 +2830,7 @@ update_action(void *arg) { "prerequisite not satisfied"); } } - } else if (update_class == zoneclass) { + } else if (update_class == dns_rdataclass_in) { /* "temp += rr;" */ result = temp_append(&temp, name, &rdata); if (result != ISC_R_SUCCESS) { @@ -2887,10 +2892,10 @@ update_action(void *arg) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &name, &rdata, + &covers, &ttl, &update_class); - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * RFC1123 doesn't allow MF and MD in master files. */ From 39a4ad2330fdd4b95e919c398f00694183e6d79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 4 Mar 2026 10:46:58 +0100 Subject: [PATCH 03/30] Validate DNS message CLASS early in request processing Reject requests with unsupported or misused CLASS values before further processing. Only IN, CH, HS, RESERVED0 (for DNS Cookies), ANY (for TKEY negotiation), and NONE (for DNS UPDATE) are accepted; all other classes return NOTIMP. Misuse of NONE or ANY outside their allowed contexts returns FORMERR. This adds further protection against bugs of the same general class as YWH-PGM40640-70 and YWH-PGM40640-73. (cherry picked from commit 0a687451505037e9f9a850c9cb113aed4995b03f) --- bin/tests/system/unknown/tests.sh | 17 ++++++---- lib/ns/client.c | 55 ++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/bin/tests/system/unknown/tests.sh b/bin/tests/system/unknown/tests.sh index eb61f21f28..cbc2943f17 100644 --- a/bin/tests/system/unknown/tests.sh +++ b/bin/tests/system/unknown/tests.sh @@ -25,6 +25,11 @@ dig_cmd() { "$DIG" $DIGOPTS "$@" | grep -v '^;' } +dig_full() { + # shellcheck disable=SC2086 + "$DIG" $DIGOPTS "$@" +} + n=$((n + 1)) echo_i "querying for various representations of an IN A record ($n)" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do @@ -81,8 +86,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE1 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n - echo '\# 4 0A000001' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -93,8 +98,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TXT record ($n)" for i in 1 2 3 4; do ret=0 - dig_cmd +short @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n - echo '"hello"' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -105,8 +110,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE123 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n - echo '\# 1 00' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi diff --git a/lib/ns/client.c b/lib/ns/client.c index 32f0240180..c10b122629 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -2041,7 +2042,9 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, } } - if (client->message->rdclass == 0) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + switch (client->message->rdclass) { + case dns_rdataclass_reserved0: if ((client->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && client->message->opcode == dns_opcode_query && client->message->counts[DNS_SECTION_QUESTION] == 0U) @@ -2060,12 +2063,46 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, return; } + ns_client_dumpmessage(client, + "message class could not be determined"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + return; + case dns_rdataclass_in: + break; + case dns_rdataclass_chaos: + break; + case dns_rdataclass_hs: + break; + case dns_rdataclass_none: + if (client->message->opcode != dns_opcode_update) { + ns_client_dumpmessage(client, + "message class NONE can be only " + "used in DNS updates"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + case dns_rdataclass_any: + /* + * Required for TKEY negotiation. + */ + if (client->message->tkey == 0) { + ns_client_dumpmessage(client, + "message class ANY can be only " + "used for TKEY negotiation"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + default: + dns_rdataclass_format(client->message->rdclass, classbuf, + sizeof(classbuf)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), - "message class could not be determined"); - ns_client_dumpmessage(client, "message class could not be " - "determined"); - ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + "invalid message class: %s", classbuf); + + ns_client_error(client, DNS_R_NOTIMP); return; } @@ -2149,9 +2186,6 @@ ns_client_request_continue(void *arg) { "SIG(0) checks quota reached"); if (can_log_sigchecks_quota()) { - ns_client_log(client, NS_LOGCATEGORY_CLIENT, - NS_LOGMODULE_CLIENT, ISC_LOG_INFO, - "SIG(0) checks quota reached"); ns_client_dumpmessage( client, "SIG(0) checks quota reached"); } @@ -2161,12 +2195,11 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", classname); - ns_client_dumpmessage(client, - "no matching view in class"); } dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); @@ -2804,7 +2837,7 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) { int len = 1024; isc_result_t result; - if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1))) { + if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1)) || reason == NULL) { return; } From 1bedd7f2441f2d81f2f45b9bfd5c2853d3f44e5c Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 09:46:01 +0100 Subject: [PATCH 04/30] Limit the number of addresses returned per ADB find Add a hard limit on the number of addresses that ADB returns from a single NS lookup (dns_adbfind_t). This mitigates a flood attack where an attacker controls a zone with many addresses for a nameserver, each returning an invalid response. The global max-query count (default 50) also limits this, but significant harm can be done before that limit is reached. The default limit is now 6 (v4 and/or v6) addresses for an ADB find (so, ADB looking up for A/AAAA addresses of a name server name). It can be overridden for testing via 'named -T adbaddrslimit=N'. (cherry picked from commit 3ec37fc69356ee682bee7f67940613ac31d93d7b) --- bin/named/main.c | 8 ++++++++ lib/dns/adb.c | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/bin/named/main.c b/bin/named/main.c index f3b2ec80f5..b298d6e98d 100644 --- a/bin/named/main.c +++ b/bin/named/main.c @@ -123,6 +123,7 @@ extern unsigned int dns_zone_mkey_month; extern unsigned int dns_adb_entrywindow; extern unsigned int dns_adb_cachemin; extern size_t dns_dispatch_tcppipelining; +extern size_t dns_adb_addrslimit; static bool want_stats = false; static char program_name[NAME_MAX] = "named"; @@ -817,6 +818,13 @@ parse_T_opt(char *option) { "least 1"); } dns_dispatch_tcppipelining = pipelining; + } else if (!strncmp(option, "adbaddrslimit=", 14)) { + size_t adb_addrslimit = atoi(option + 14); + if (adb_addrslimit < 1) { + named_main_earlyfatal("adbaddrslimit must be at " + "least 1"); + } + dns_adb_addrslimit = adb_addrslimit; } else { fprintf(stderr, "unknown -T flag '%s'\n", option); } diff --git a/lib/dns/adb.c b/lib/dns/adb.c index 50d4d83bf3..1514fb46b9 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -78,6 +78,15 @@ #define DNS_ADB_MINADBSIZE (1024U * 1024U) /*%< 1 Megabyte */ +/* + * Default and override for the per-find address limit, the sum of the number of + * A and AAAA RR from an ADB NS name resolution. When non-zero, this value is + * used instead of the default. Can be set via 'named -T adbaddrslimit=N' for + * testing. + */ +#define DEFAULT_ADDRSLIMIT 6 +size_t dns_adb_addrslimit = 0; + typedef ISC_LIST(dns_adbname_t) dns_adbnamelist_t; typedef struct dns_adbnamehook dns_adbnamehook_t; typedef ISC_LIST(dns_adbnamehook_t) dns_adbnamehooklist_t; @@ -1473,6 +1482,9 @@ static void copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { dns_adbnamehook_t *namehook = NULL; dns_adbentry_t *entry = NULL; + size_t count = 0; + size_t limit = dns_adb_addrslimit != 0 ? dns_adb_addrslimit + : DEFAULT_ADDRSLIMIT; if ((find->options & DNS_ADBFIND_INET) != 0) { namehook = ISC_LIST_HEAD(name->v4); @@ -1493,6 +1505,12 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { * Found a valid entry. Add it to the find's list. */ ISC_LIST_APPEND(find->list, addrinfo, publink); + + if (++count >= limit) { + DP(ISC_LOG_DEBUG(3), "skipping addresses"); + return; + } + nextv4: namehook = ISC_LIST_NEXT(namehook, name_link); } @@ -1517,6 +1535,12 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { * Found a valid entry. Add it to the find's list. */ ISC_LIST_APPEND(find->list, addrinfo, publink); + + if (++count >= limit) { + DP(ISC_LOG_DEBUG(3), "skipping addresses"); + return; + } + nextv6: namehook = ISC_LIST_NEXT(namehook, name_link); } From 7577e00479231eebe8fb996b460a9c4554c5b84a Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 9 Apr 2026 11:32:07 +0200 Subject: [PATCH 05/30] Add reproducer for BADCOOKIE resend loop Run malicious server: resend_loop/ans3/ans.py Start BIND: ns4 Send single query to test.example The resolver will repeatedly resend queries until the fetch timeout expires, resulting in resulting in thousands of qrysent while the quota counter remains 0. (cherry picked from commit 7eeb463bc58cbd71419aaf189d7829f2dfd8d055) --- bin/tests/system/resend_loop/ans3/ans.py | 126 ++++++++++++++++++ .../system/resend_loop/ns4/named.conf.j2 | 16 +++ bin/tests/system/resend_loop/ns4/root.hint | 14 ++ .../system/resend_loop/tests_resend_loop.py | 28 ++++ 4 files changed, 184 insertions(+) create mode 100644 bin/tests/system/resend_loop/ans3/ans.py create mode 100644 bin/tests/system/resend_loop/ns4/named.conf.j2 create mode 100644 bin/tests/system/resend_loop/ns4/root.hint create mode 100644 bin/tests/system/resend_loop/tests_resend_loop.py diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py new file mode 100644 index 0000000000..217bae0301 --- /dev/null +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -0,0 +1,126 @@ +# 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. + +from collections.abc import AsyncGenerator + +import dns.edns +import dns.name +import dns.rcode +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + + +def _get_cookie(qctx: QueryContext): + for o in qctx.query.options: + if o.otype == dns.edns.OptionType.COOKIE: + cookie = o + try: + if len(cookie.server) == 0: + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" + except AttributeError: # dnspython<2.7.0 compat + if len(o.data) == 8: + cookie.data *= 2 + + return cookie + + return None + + +class PrimeHandler(ResponseHandler): + """ + Specifically handle priming query for "." NS (type 2) + """ + + def match(self, qctx: QueryContext) -> bool: + return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + ns_rrset = dns.rrset.from_text( + ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil." + ) + a_rrset = dns.rrset.from_text( + "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3" + ) + + response = qctx.prepare_new_response(with_zone_data=False) + response.set_rcode(dns.rcode.NOERROR) + response.answer.append(ns_rrset) + response.additional.append(a_rrset) + + yield DnsResponseSend(response, authoritative=True) + + +class CookieHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + example = dns.name.from_text("example") + return qctx.qname.is_subdomain(example) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + + # Check for client cookie + cookie = _get_cookie(qctx) + + # If missing cookie entirely, just return SERVFAIL + if cookie is None: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + yield DnsResponseSend(qctx.response, authoritative=True) + + # If there is a client cookie, mock BADCOOKIE to trigger + # the resend loop logic. + qctx.response.use_edns(options=[cookie]) + qctx.response.set_rcode(dns.rcode.BADCOOKIE) + yield DnsResponseSend(qctx.response, authoritative=True) + + +class NoErrorHandler(ResponseHandler): + """ + If the query is NOT a subdomain of example, respond with standard NOERROR empty answer + """ + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + qctx.response.set_rcode(dns.rcode.NOERROR) + yield DnsResponseSend(qctx.response, authoritative=True) + + +def resend_server() -> AsyncDnsServer: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + PrimeHandler(), + CookieHandler(), + NoErrorHandler(), + ) + return server + + +def main() -> None: + resend_server().run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2 new file mode 100644 index 0000000000..360bc12e17 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/named.conf.j2 @@ -0,0 +1,16 @@ +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; +}; + +zone "." IN { + type hint; + file "root.hint"; +}; diff --git a/bin/tests/system/resend_loop/ns4/root.hint b/bin/tests/system/resend_loop/ns4/root.hint new file mode 100644 index 0000000000..3889a8b353 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/root.hint @@ -0,0 +1,14 @@ +; 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 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.3 diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py new file mode 100644 index 0000000000..f7ed4d3da6 --- /dev/null +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -0,0 +1,28 @@ +# 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.message + +import isctest + + +def test_resend_loop_badcookie(ns4): + expected_log = "exceeded max queries resolving 'test.example/A'" + + msg = dns.message.make_query("test.example", "A") + with ns4.watch_log_from_here() as watcher: + res = isctest.query.udp(msg, ns4.ip) + watcher.wait_for_line(expected_log) + + isctest.check.servfail(res) + + prohibited_log = "query failed (timed out) for test.example/IN/A" + assert prohibited_log not in ns4.log From 85edb6b18491675279f8501f3bf5d1b0a87f04dc Mon Sep 17 00:00:00 2001 From: Alessio Podda Date: Mon, 13 Apr 2026 15:55:38 +0200 Subject: [PATCH 06/30] Add xfr quota starvation system test Add a starvation test that tries to starve the XFR quota with unautorized requests. (cherry picked from commit 53135592b7ff6c272b6577b2e7747258628442e3) --- bin/tests/system/xferquota/ns1/named.conf.j2 | 2 + bin/tests/system/xferquota/ns3/named.conf.j2 | 46 +++++++++++++++++++ bin/tests/system/xferquota/ns3/quota.db | 22 +++++++++ bin/tests/system/xferquota/ns3/root.db | 21 +++++++++ bin/tests/system/xferquota/tests_xferquota.py | 42 +++++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 bin/tests/system/xferquota/ns3/named.conf.j2 create mode 100644 bin/tests/system/xferquota/ns3/quota.db create mode 100644 bin/tests/system/xferquota/ns3/root.db diff --git a/bin/tests/system/xferquota/ns1/named.conf.j2 b/bin/tests/system/xferquota/ns1/named.conf.j2 index 67f2e63f40..c367254469 100644 --- a/bin/tests/system/xferquota/ns1/named.conf.j2 +++ b/bin/tests/system/xferquota/ns1/named.conf.j2 @@ -23,6 +23,8 @@ options { recursion no; dnssec-validation no; notify yes; + + transfers-out 3; }; key rndc_key { diff --git a/bin/tests/system/xferquota/ns3/named.conf.j2 b/bin/tests/system/xferquota/ns3/named.conf.j2 new file mode 100644 index 0000000000..4a0d70ca55 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/named.conf.j2 @@ -0,0 +1,46 @@ +/* + * 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.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 no; + dnssec-validation no; + + transfers-out 1; + allow-transfer { 10.53.0.2; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "." { + type primary; + file "root.db"; +}; + +zone "quota." { + type primary; + file "quota.db"; +}; diff --git a/bin/tests/system/xferquota/ns3/quota.db b/bin/tests/system/xferquota/ns3/quota.db new file mode 100644 index 0000000000..12a67d3d2a --- /dev/null +++ b/bin/tests/system/xferquota/ns3/quota.db @@ -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 300 +@ IN SOA ns1.quota. hostmaster.quota. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) + IN NS ns1.quota. +ns1 IN A 10.53.0.3 +www IN A 10.0.0.1 diff --git a/bin/tests/system/xferquota/ns3/root.db b/bin/tests/system/xferquota/ns3/root.db new file mode 100644 index 0000000000..a5ff0fc697 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/root.db @@ -0,0 +1,21 @@ +; 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 ns.root. hostmaster.root. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.3 diff --git a/bin/tests/system/xferquota/tests_xferquota.py b/bin/tests/system/xferquota/tests_xferquota.py index 6d255085cb..5d394d070e 100644 --- a/bin/tests/system/xferquota/tests_xferquota.py +++ b/bin/tests/system/xferquota/tests_xferquota.py @@ -12,12 +12,15 @@ from re import compile as Re import glob +import multiprocessing import os import re import shutil import signal import time +import dns.message +import dns.query import dns.zone import pytest @@ -60,6 +63,9 @@ def test_xferquota(named_port, ns1, ns2): matching_line_count += 1 return matching_line_count == 300 + # The primary has 'transfers-out 3;', while the secondary has + # 'transfers-in 5; transfer-per-ns 5;'. This will allow all the zones + # to be eventually transferred, hitting the quotas now and then. isctest.run.retry_with_timeout(check_line_count, timeout=360) axfr_msg = isctest.query.create("zone000099.example.", "AXFR") @@ -80,3 +86,39 @@ def test_xferquota(named_port, ns1, ns2): with ns2.watch_log_from_start(timeout=30) as watcher: watcher.wait_for_line(pattern) query_and_compare(a_msg) + + +def _flood_unauthorized_axfrs(port, duration): + """Child process: send unauthorized AXFR requests for `duration` seconds.""" + deadline = time.monotonic() + duration + while time.monotonic() < deadline: + try: + msg = dns.message.make_query("quota.", "AXFR") + dns.query.tcp(msg, "10.53.0.3", port=port, timeout=2, source="10.53.0.1") + except Exception: # pylint: disable=broad-exception-caught + pass + + +def test_xfrquota_unauthorized_no_starve(named_port): + """Unauthorized AXFR clients must not consume XFR-out quota (GL #3859). + + ns3 is configured with transfers-out 1 and allow-transfer { 10.53.0.2; }. + We flood AXFR requests from unauthorized source processes (10.53.0.1) and + verify that an authorized client (10.53.0.2) can still transfer. + """ + with multiprocessing.Pool(10) as pool: + pool.starmap_async(_flood_unauthorized_axfrs, [(named_port, 5)] * 10) + + # Give the flood a moment to saturate + time.sleep(1) + + # Try an authorized AXFR from 10.53.0.2 multiple times to increase + # the chance of hitting the race window where quota is consumed. + zone = dns.zone.Zone("quota.") + dns.query.inbound_xfr( + "10.53.0.3", + zone, + port=named_port, + timeout=10, + source="10.53.0.2", + ) From a40325c6a27c728c27becc6dc91513b5b953177d Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Wed, 4 Mar 2026 10:00:56 +1100 Subject: [PATCH 07/30] Reject meta-classes in UPDATE and NOTIFY messages NOTIFY and UPDATE messages must specify a data class in the QUESTION/ZONE section. NONE and ANY are meta-classes and not appropriate here. Return FORMERR if either is used. Rejecting messages with a query class of NONE addresses YWH-PGM40640-72, YWH-PGM40640-82, and YWH-PGM40640-83. Rejecting messages with a query class of ANY addresses YWH-PGM40640-87, YWH-PGM40640-88, and YWH-PGM40640-117. Fixes: isc-projects/bind9#5778 Fixes: isc-projects/bind9#5782 Fixes: isc-projects/bind9#5783 Fixes: isc-projects/bind9#5797 Fixes: isc-projects/bind9#5798 Fixes: isc-projects/bind9#5853 (cherry picked from commit c66a1b1e1bfd6c79d7b9bc8d4a59e69f4faa1563) --- lib/dns/message.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/dns/message.c b/lib/dns/message.c index 19bb6a8530..dce9c2d65f 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -1073,6 +1073,17 @@ getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, rdtype = isc_buffer_getuint16(source); rdclass = isc_buffer_getuint16(source); + /* + * Notify and update messages need to specify the data class. + */ + if ((msg->opcode == dns_opcode_update || + msg->opcode == dns_opcode_notify) && + (rdclass == dns_rdataclass_none || + rdclass == dns_rdataclass_any)) + { + DO_ERROR(DNS_R_FORMERR); + } + /* * If this class is different than the one we already read, * this is an error. From b456007e2d9af38d128bbb1b19c2bbfe0de7a475 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Feb 2026 10:18:42 +0100 Subject: [PATCH 08/30] Remove duplicate addresses from the resolver SLIST The SLIST (essentially `fctx->finds`, forwarders and dual-stack alternatives aside) can have duplicate server addresses when multiple in-domain nameservers share the same IP addresses: sub.example. NS ns1.sub.example. sub.example. NS ns2.sub.example. ns1.sub.example. A 1.2.3.4 ns1.sub.example. A 5.6.7.8 ns2.sub.example. A 1.2.3.4 ns2.sub.example. A 5.6.7.8 If both 1.2.3.4 and 5.6.7.8 fail to return a valid answer, the resolver would query each address twice. The problem is fixed by replacing the two-phase server selection (sort each find list by SRTT, sort finds by head SRTT) with a single linear scan in nextaddress() that finds the lowest-SRTT unmarked, non-duplicate address across all find lists. The old approach had a correctness bug: after sorting, the resolver picked the next address from the "current" find list rather than globally. For example, with find lists [1, 15, 26] and [3, 4, 5], the second pick would be SRTT 15 instead of the correct SRTT 3. The new approach is both simpler and correct: each call to nextaddress() walks all addresses, skips marked and duplicate entries, and returns the one with the lowest SRTT. While this walk is repeated for each server attempt, it operates on a small bounded list and is negligible compared to the network I/O of querying the server. (cherry picked from commit b1c5856a3764b4025e93f8baf06c45c8fa029752) --- lib/dns/resolver.c | 220 ++++++++++++++++++--------------------------- 1 file changed, 89 insertions(+), 131 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 6d6fb8eba3..8b76b7da23 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -374,7 +374,16 @@ struct fetchctx { dns_message_t *qmessage; ISC_LIST(resquery_t) queries; dns_adbfindlist_t finds; - dns_adbfind_t *find; + /* + * This is a state to keep track of the latest upstream server which is + * being queried. See `nextaddress()`. + * + * `addrinfo` is basically a copy of `foundaddrinfo` but came from the + * response of the query, so fields like the SRTT/timing might have been + * altered. So it might be possible (?) to wrap those two in an union + * for clarity (and memory saving). + */ + dns_adbaddrinfo_t *foundaddrinfo; /* * altfinds are names and/or addresses of dual stack servers that * should be used when iterative resolution to a server is not @@ -1314,7 +1323,7 @@ fctx_cleanup(fetchctx_t *fctx) { dns_adb_destroyfind(&find); fetchctx_unref(fctx); } - fctx->find = NULL; + fctx->foundaddrinfo = NULL; for (find = ISC_LIST_HEAD(fctx->altfinds); find != NULL; find = next_find) @@ -3164,89 +3173,6 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo, isc_result_totext(reason), namebuf, typebuf, classbuf, addrbuf); } -/* - * Sort addrinfo list by RTT. - */ -static void -sort_adbfind(dns_adbfind_t *find, unsigned int bias) { - dns_adbaddrinfo_t *best, *curr; - dns_adbaddrinfolist_t sorted; - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(find->list)) { - unsigned int best_srtt; - best = ISC_LIST_HEAD(find->list); - best_srtt = best->srtt; - if (isc_sockaddr_pf(&best->sockaddr) != AF_INET6) { - best_srtt += bias; - } - curr = ISC_LIST_NEXT(best, publink); - while (curr != NULL) { - unsigned int curr_srtt = curr->srtt; - if (isc_sockaddr_pf(&curr->sockaddr) != AF_INET6) { - curr_srtt += bias; - } - if (curr_srtt < best_srtt) { - best = curr; - best_srtt = curr_srtt; - } - curr = ISC_LIST_NEXT(curr, publink); - } - ISC_LIST_UNLINK(find->list, best, publink); - ISC_LIST_APPEND(sorted, best, publink); - } - find->list = sorted; -} - -/* - * Sort a list of finds by server RTT. - */ -static void -sort_finds(dns_adbfindlist_t *findlist, unsigned int bias) { - dns_adbfind_t *best, *curr; - dns_adbfindlist_t sorted; - dns_adbaddrinfo_t *addrinfo, *bestaddrinfo; - - /* Sort each find's addrinfo list by SRTT. */ - for (curr = ISC_LIST_HEAD(*findlist); curr != NULL; - curr = ISC_LIST_NEXT(curr, publink)) - { - sort_adbfind(curr, bias); - } - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(*findlist)) { - unsigned int best_srtt; - best = ISC_LIST_HEAD(*findlist); - bestaddrinfo = ISC_LIST_HEAD(best->list); - INSIST(bestaddrinfo != NULL); - best_srtt = bestaddrinfo->srtt; - if (isc_sockaddr_pf(&bestaddrinfo->sockaddr) != AF_INET6) { - best_srtt += bias; - } - curr = ISC_LIST_NEXT(best, publink); - while (curr != NULL) { - unsigned int curr_srtt; - addrinfo = ISC_LIST_HEAD(curr->list); - INSIST(addrinfo != NULL); - curr_srtt = addrinfo->srtt; - if (isc_sockaddr_pf(&addrinfo->sockaddr) != AF_INET6) { - curr_srtt += bias; - } - if (curr_srtt < best_srtt) { - best = curr; - best_srtt = curr_srtt; - } - curr = ISC_LIST_NEXT(curr, publink); - } - ISC_LIST_UNLINK(*findlist, best, publink); - ISC_LIST_APPEND(sorted, best, publink); - } - *findlist = sorted; -} - /* * Return true iff the ADB find has an already pending fetch for 'type'. This * is used to find out whether we're in a loop, where a fetch is waiting for a @@ -3367,6 +3293,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, } } } + if ((flags & FCTX_ADDRINFO_DUALSTACK) != 0) { ISC_LIST_APPEND(fctx->altfinds, find, publink); } else { @@ -3841,8 +3768,6 @@ out: * We've found some addresses. We might still be * looking for more addresses. */ - sort_finds(&fctx->finds, res->view->v6bias); - sort_finds(&fctx->altfinds, 0); result = ISC_R_SUCCESS; } @@ -3914,6 +3839,80 @@ possibly_mark(fetchctx_t *fctx, dns_adbaddrinfo_t *addr) { } } +static dns_adbaddrinfo_t * +nextaddress(fetchctx_t *fctx) { + dns_adbaddrinfo_t *prevai = fctx->foundaddrinfo, *lowestsrttai = NULL; + unsigned int v6bias = fctx->res->view->v6bias, lowestsrtt = 0; + + /* + * Let's walk through the list of dns_adbaddrinfo_t to find the best + * next server address to query. This is linear on the number of + * dns_adbaddrinfo_t which are grouped in find list (for each ADB find). + */ + for (dns_adbfind_t *find = ISC_LIST_HEAD(fctx->finds); find != NULL; + find = ISC_LIST_NEXT(find, publink)) + { + for (dns_adbaddrinfo_t *ai = ISC_LIST_HEAD(find->list); + ai != NULL; ai = ISC_LIST_NEXT(ai, publink)) + { + /* + * This address has been marked already, skip it. + */ + if (!UNMARKED(ai)) { + continue; + } + + /* + * This address is the same as the previously used + * address, it's a duplicate, mark it and skip it! + */ + if (prevai != NULL) { + if (prevai->entry == ai->entry) { + ai->flags |= FCTX_ADDRINFO_MARK; + continue; + } + } + + /* + * Mark and skip this address if incompatible (i.e. IPv6 + * address on a v4 only server, or for ACL reason, etc.) + */ + possibly_mark(fctx, ai); + if (!UNMARKED(ai)) { + continue; + } + + /* + * This address hasn't been tried yet and is a + * good candidate. Let's keep track of it if it + * has the lowest SRTT so far (or if there is no + * address with lowest SRTT found yet). + */ + unsigned int aisrtt = ai->srtt; + + if (isc_sockaddr_pf(&ai->sockaddr) != AF_INET6) { + aisrtt += v6bias; + } + + if (lowestsrttai == NULL || aisrtt < lowestsrtt) { + lowestsrttai = ai; + lowestsrtt = aisrtt; + continue; + } + } + } + + /* + * This is the next address to query. If this is NULL, we're done. + */ + if (lowestsrttai != NULL) { + lowestsrttai->flags |= FCTX_ADDRINFO_MARK; + } + fctx->foundaddrinfo = lowestsrttai; + + return lowestsrttai; +} + static dns_adbaddrinfo_t * fctx_nextaddress(fetchctx_t *fctx) { dns_adbfind_t *find, *start; @@ -3936,7 +3935,6 @@ fctx_nextaddress(fetchctx_t *fctx) { possibly_mark(fctx, addrinfo); if (UNMARKED(addrinfo)) { addrinfo->flags |= FCTX_ADDRINFO_MARK; - fctx->find = NULL; fctx->forwarding = true; /* @@ -3957,49 +3955,9 @@ fctx_nextaddress(fetchctx_t *fctx) { fctx->forwarding = false; FCTX_ATTR_SET(fctx, FCTX_ATTR_TRIEDFIND); - find = fctx->find; - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } else { - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } - - /* - * Find the first unmarked addrinfo. - */ - addrinfo = NULL; - if (find != NULL) { - start = find; - do { - for (addrinfo = ISC_LIST_HEAD(find->list); - addrinfo != NULL; - addrinfo = ISC_LIST_NEXT(addrinfo, publink)) - { - if (!UNMARKED(addrinfo)) { - continue; - } - possibly_mark(fctx, addrinfo); - if (UNMARKED(addrinfo)) { - addrinfo->flags |= FCTX_ADDRINFO_MARK; - break; - } - } - if (addrinfo != NULL) { - break; - } - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } while (find != start); - } - - fctx->find = find; - if (addrinfo != NULL) { - return addrinfo; + faddrinfo = nextaddress(fctx); + if (faddrinfo != NULL) { + return faddrinfo; } /* From ae554715ae4607d02c643ae4898ce1babe95915b Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Tue, 7 Apr 2026 22:18:10 +0200 Subject: [PATCH 09/30] Refactor incrementing query counters Move the logic incrementing the query counter and the global query counter into a dedicated helper function. (cherry picked from commit 05d6da2de54c093689e675e81ae898ee41220666) --- lib/dns/resolver.c | 60 ++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 6bab8057fb..50d781b361 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -4080,6 +4080,39 @@ fctx_nextaddress(fetchctx_t *fctx) { return addrinfo; } +static isc_result_t +incr_query_counters(fetchctx_t *fctx) { + isc_result_t result; + + result = isc_counter_increment(fctx->qc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-recursion-queries, querycount=", + isc_counter_used(fctx->qc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), + "exceeded max queries resolving '%s' " + "(max-recursion-queries, querycount=%u)", + fctx->info, isc_counter_used(fctx->qc)); + } else if (fctx->gqc != NULL) { + result = isc_counter_increment(fctx->gqc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-query-count, querycount=", + isc_counter_used(fctx->gqc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), + "exceeded global max queries resolving " + "'%s' (max-query-count, querycount=%u)", + fctx->info, isc_counter_used(fctx->gqc)); + } + } + + return result; +} + static void fctx_try(fetchctx_t *fctx, bool retrying) { isc_result_t result; @@ -4220,36 +4253,11 @@ fctx_try(fetchctx_t *fctx, bool retrying) { return; } - result = isc_counter_increment(fctx->qc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-recursion-queries, querycount=", - isc_counter_used(fctx->qc)); -#endif + result = incr_query_counters(fctx); if (result != ISC_R_SUCCESS) { - isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, - DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), - "exceeded max queries resolving '%s' " - "(max-recursion-queries, querycount=%u)", - fctx->info, isc_counter_used(fctx->qc)); goto done; } - if (fctx->gqc != NULL) { - result = isc_counter_increment(fctx->gqc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-query-count, querycount=", - isc_counter_used(fctx->gqc)); -#endif - if (result != ISC_R_SUCCESS) { - isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, - DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), - "exceeded global max queries resolving " - "'%s' (max-query-count, querycount=%u)", - fctx->info, isc_counter_used(fctx->gqc)); - goto done; - } - } - result = fctx_query(fctx, addrinfo, fctx->options); if (result != ISC_R_SUCCESS) { goto done; From 69fb85d9946b71297978135bee24db7bb99ade5f Mon Sep 17 00:00:00 2001 From: Aram Sargsyan Date: Tue, 31 Mar 2026 13:00:00 +0000 Subject: [PATCH 10/30] Apply XFR-out quota after ACL is checked Unauthorized clients can consume XFR-out quota and block authorized XFR clients. Apply the quota after ACL is checked. (cherry picked from commit 5615e6c47a2cd00d82d48b568cc55a4b89daa330) --- lib/ns/xfrout.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/ns/xfrout.c b/lib/ns/xfrout.c index 71085df787..9772767b00 100644 --- a/lib/ns/xfrout.c +++ b/lib/ns/xfrout.c @@ -744,6 +744,7 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { bool is_poll = false; bool is_dlz = false; bool is_ixfr = false; + bool is_quota_applied = false; bool useviewacl = false; uint32_t begin_serial = 0, current_serial; @@ -760,16 +761,6 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(6), "%s request", mnemonic); - /* - * Apply quota. - */ - result = isc_quota_acquire(&client->manager->sctx->xfroutquota); - if (result != ISC_R_SUCCESS) { - isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, - "%s request denied: %s", mnemonic, - isc_result_totext(result)); - goto max_quota; - } /* * Interpret the question section. @@ -940,6 +931,19 @@ got_soa: FAILC(DNS_R_FORMERR, "attempted AXFR over UDP"); } + /* + * Apply quota after ACL is checked, so that unauthorized clients + * can not starve the authorized clients. + */ + result = isc_quota_acquire(&client->manager->sctx->xfroutquota); + if (result != ISC_R_SUCCESS) { + isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, + "%s request denied: %s", mnemonic, + isc_result_totext(result)); + goto cleanup; + } + is_quota_applied = true; + /* * Look up the requesting server in the peer table. */ @@ -1078,7 +1082,7 @@ have_stream: CHECK(dns_message_getquerytsig(request, mctx, &tsigbuf)); /* * Create the xfrout context object. This transfers the ownership - * of "stream", "db", "ver", and "quota" to the xfrout context object. + * of "stream", "db" and "ver" to the xfrout context object. */ if (is_dlz) { @@ -1193,10 +1197,13 @@ cleanup: } if (xfr != NULL) { + /* The quota will be released in xfrout_ctx_destroy(). */ + INSIST(is_quota_applied); xfrout_fail(xfr, result, "setting up zone transfer"); } else if (result != ISC_R_SUCCESS) { - isc_quota_release(&client->manager->sctx->xfroutquota); - max_quota: + if (is_quota_applied) { + isc_quota_release(&client->manager->sctx->xfroutquota); + } ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(3), "zone transfer setup failed"); From ec1404cc4c124b456409e1dbe9d16cdbe31c68c9 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:24:43 -0700 Subject: [PATCH 11/30] Skip "deny-answer-address" for non-IN addresses Ensure that we don't attempt an ACL match for answer addresses when handling a class-CHAOS zone. This is an additional line of defense for YWH-PGM40640-74. (cherry picked from commit e62673c765b52307c800e86f0185fe52b573c145) --- lib/dns/resolver.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 87f423a69b..4b70963a96 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -6967,6 +6967,13 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, return true; } + /* + * deny-answer-address doesn't apply to non-IN classes. + */ + if (rdataset->rdclass != dns_rdataclass_in) { + return true; + } + /* * Otherwise, search the filter list for a match for each * address record. If a match is found, the address should be From 4340b3537de687d9606aebc4107fa06ebe8888ae Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 11:20:11 +0100 Subject: [PATCH 12/30] Add system test for self-pointed glue deduplication Test the resolver's behavior with self-pointed glue where each NS has the same set of addresses. Verify that addresses are deduplicated and each unique IP is only queried once. Also test the ADB address limit knob (-T adbaddrslimit=). (cherry picked from commit c21fc6cb95d77312d6fb891f17ce9df41a25af6d) --- .../system/selfpointedglue/ns1/named.conf.j2 | 28 ++++ bin/tests/system/selfpointedglue/ns1/root.db | 24 +++ .../system/selfpointedglue/ns2/named.conf.j2 | 28 ++++ bin/tests/system/selfpointedglue/ns2/tld.db | 27 +++ .../system/selfpointedglue/ns3/example.tld.db | 155 ++++++++++++++++++ .../selfpointedglue/ns3/example2.tld.db | 33 ++++ .../system/selfpointedglue/ns3/named.conf.j2 | 44 +++++ .../system/selfpointedglue/ns4/named.args.j2 | 3 + .../system/selfpointedglue/ns4/named.conf.j2 | 59 +++++++ .../system/selfpointedglue/ns4/root.hint | 14 ++ .../selfpointedglue/tests_selfpointedglue.py | 122 ++++++++++++++ 11 files changed, 537 insertions(+) create mode 100644 bin/tests/system/selfpointedglue/ns1/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns1/root.db create mode 100644 bin/tests/system/selfpointedglue/ns2/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns2/tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/example.tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/example2.tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/named.args.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/root.hint create mode 100644 bin/tests/system/selfpointedglue/tests_selfpointedglue.py diff --git a/bin/tests/system/selfpointedglue/ns1/named.conf.j2 b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 new file mode 100644 index 0000000000..fd83fc3c19 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 @@ -0,0 +1,28 @@ +/* + * 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; }; + recursion no; + dnssec-validation no; +}; + +zone "." { + type primary; + file "root.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns1/root.db b/bin/tests/system/selfpointedglue/ns1/root.db new file mode 100644 index 0000000000..bfbf049b80 --- /dev/null +++ b/bin/tests/system/selfpointedglue/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 owner.root-servers.nil. a.root.servers.nil. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +tld. NS ns.tld. +ns.tld. A 10.53.0.2 diff --git a/bin/tests/system/selfpointedglue/ns2/named.conf.j2 b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 new file mode 100644 index 0000000000..2993832da2 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 @@ -0,0 +1,28 @@ +/* + * 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; }; + recursion no; + dnssec-validation no; +}; + +zone "tld." { + type primary; + file "tld.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns2/tld.db b/bin/tests/system/selfpointedglue/ns2/tld.db new file mode 100644 index 0000000000..5935fd841c --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/tld.db @@ -0,0 +1,27 @@ +; 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 +tld. IN SOA owner.tld. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +tld. NS ns.tld. +ns.tld. A 10.53.0.2 + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + +example2.tld. NS ns.example2.tld. +ns.example2.tld. A 10.53.0.3 diff --git a/bin/tests/system/selfpointedglue/ns3/example.tld.db b/bin/tests/system/selfpointedglue/ns3/example.tld.db new file mode 100644 index 0000000000..83ea4d37ec --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example.tld.db @@ -0,0 +1,155 @@ +; 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 +example.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + +sub.example.tld. NS ns01.sub.example.tld. +sub.example.tld. NS ns02.sub.example.tld. +sub.example.tld. NS ns03.sub.example.tld. +sub.example.tld. NS ns04.sub.example.tld. +sub.example.tld. NS ns05.sub.example.tld. +sub.example.tld. NS ns06.sub.example.tld. +sub.example.tld. NS ns07.sub.example.tld. +sub.example.tld. NS ns08.sub.example.tld. +sub.example.tld. NS ns09.sub.example.tld. +sub.example.tld. NS ns10.sub.example.tld. + +ns01.sub.example.tld. A 10.53.0.5 +ns01.sub.example.tld. A 10.53.0.6 +ns01.sub.example.tld. A 10.53.0.7 +ns01.sub.example.tld. A 10.53.0.8 +ns01.sub.example.tld. A 10.53.0.9 +ns01.sub.example.tld. A 10.53.0.10 +ns01.sub.example.tld. A 10.53.1.1 +ns01.sub.example.tld. A 10.53.1.2 +ns01.sub.example.tld. A 10.53.2.1 +ns01.sub.example.tld. A 10.53.0.3 +; Those RR (same below) pointing to 127.0.0.1 won't ever be used as they +; exceeded the ADB limit. +ns01.sub.example.tld. A 127.0.0.1 + +ns02.sub.example.tld. A 10.53.0.5 +ns02.sub.example.tld. A 10.53.0.6 +ns02.sub.example.tld. A 10.53.0.7 +ns02.sub.example.tld. A 10.53.0.8 +ns02.sub.example.tld. A 10.53.0.9 +ns02.sub.example.tld. A 10.53.0.10 +ns02.sub.example.tld. A 10.53.1.1 +ns02.sub.example.tld. A 10.53.1.2 +ns02.sub.example.tld. A 10.53.2.1 +ns02.sub.example.tld. A 10.53.0.3 +ns02.sub.example.tld. A 127.0.0.1 + +ns03.sub.example.tld. A 10.53.0.5 +ns03.sub.example.tld. A 10.53.0.6 +ns03.sub.example.tld. A 10.53.0.7 +ns03.sub.example.tld. A 10.53.0.8 +ns03.sub.example.tld. A 10.53.0.9 +ns03.sub.example.tld. A 10.53.0.10 +ns03.sub.example.tld. A 10.53.1.1 +ns03.sub.example.tld. A 10.53.1.2 +ns03.sub.example.tld. A 10.53.2.1 +ns03.sub.example.tld. A 10.53.0.3 +ns03.sub.example.tld. A 127.0.0.1 + +ns04.sub.example.tld. A 10.53.0.5 +ns04.sub.example.tld. A 10.53.0.6 +ns04.sub.example.tld. A 10.53.0.7 +ns04.sub.example.tld. A 10.53.0.8 +ns04.sub.example.tld. A 10.53.0.9 +ns04.sub.example.tld. A 10.53.0.10 +ns04.sub.example.tld. A 10.53.1.1 +ns04.sub.example.tld. A 10.53.1.2 +ns04.sub.example.tld. A 10.53.2.1 +ns04.sub.example.tld. A 10.53.0.3 +ns04.sub.example.tld. A 127.0.0.1 + +ns05.sub.example.tld. A 10.53.0.5 +ns05.sub.example.tld. A 10.53.0.6 +ns05.sub.example.tld. A 10.53.0.7 +ns05.sub.example.tld. A 10.53.0.8 +ns05.sub.example.tld. A 10.53.0.9 +ns05.sub.example.tld. A 10.53.0.10 +ns05.sub.example.tld. A 10.53.1.1 +ns05.sub.example.tld. A 10.53.1.2 +ns05.sub.example.tld. A 10.53.2.1 +ns05.sub.example.tld. A 10.53.0.3 +ns05.sub.example.tld. A 127.0.0.1 + +ns06.sub.example.tld. A 10.53.0.5 +ns06.sub.example.tld. A 10.53.0.6 +ns06.sub.example.tld. A 10.53.0.7 +ns06.sub.example.tld. A 10.53.0.8 +ns06.sub.example.tld. A 10.53.0.9 +ns06.sub.example.tld. A 10.53.0.10 +ns06.sub.example.tld. A 10.53.1.1 +ns06.sub.example.tld. A 10.53.1.2 +ns06.sub.example.tld. A 10.53.2.1 +ns06.sub.example.tld. A 10.53.0.3 +ns06.sub.example.tld. A 127.0.0.1 + +ns07.sub.example.tld. A 10.53.0.5 +ns07.sub.example.tld. A 10.53.0.6 +ns07.sub.example.tld. A 10.53.0.7 +ns07.sub.example.tld. A 10.53.0.8 +ns07.sub.example.tld. A 10.53.0.9 +ns07.sub.example.tld. A 10.53.0.10 +ns07.sub.example.tld. A 10.53.1.1 +ns07.sub.example.tld. A 10.53.1.2 +ns07.sub.example.tld. A 10.53.2.1 +ns07.sub.example.tld. A 10.53.0.3 +ns07.sub.example.tld. A 127.0.0.1 + +ns08.sub.example.tld. A 10.53.0.5 +ns08.sub.example.tld. A 10.53.0.6 +ns08.sub.example.tld. A 10.53.0.7 +ns08.sub.example.tld. A 10.53.0.8 +ns08.sub.example.tld. A 10.53.0.9 +ns08.sub.example.tld. A 10.53.0.10 +ns08.sub.example.tld. A 10.53.1.1 +ns08.sub.example.tld. A 10.53.1.2 +ns08.sub.example.tld. A 10.53.2.1 +ns08.sub.example.tld. A 10.53.0.3 +ns08.sub.example.tld. A 127.0.0.1 + +ns09.sub.example.tld. A 10.53.0.5 +ns09.sub.example.tld. A 10.53.0.6 +ns09.sub.example.tld. A 10.53.0.7 +ns09.sub.example.tld. A 10.53.0.8 +ns09.sub.example.tld. A 10.53.0.9 +ns09.sub.example.tld. A 10.53.0.10 +ns09.sub.example.tld. A 10.53.1.1 +ns09.sub.example.tld. A 10.53.1.2 +ns09.sub.example.tld. A 10.53.2.1 +ns09.sub.example.tld. A 10.53.0.3 +ns09.sub.example.tld. A 127.0.0.1 + +ns10.sub.example.tld. A 10.53.0.5 +ns10.sub.example.tld. A 10.53.0.6 +ns10.sub.example.tld. A 10.53.0.7 +ns10.sub.example.tld. A 10.53.0.8 +ns10.sub.example.tld. A 10.53.0.9 +ns10.sub.example.tld. A 10.53.0.10 +ns10.sub.example.tld. A 10.53.1.1 +ns10.sub.example.tld. A 10.53.1.2 +ns10.sub.example.tld. A 10.53.2.1 +ns10.sub.example.tld. A 10.53.0.3 +ns10.sub.example.tld. A 127.0.0.1 diff --git a/bin/tests/system/selfpointedglue/ns3/example2.tld.db b/bin/tests/system/selfpointedglue/ns3/example2.tld.db new file mode 100644 index 0000000000..bcab6e38c1 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example2.tld.db @@ -0,0 +1,33 @@ +; 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 +example2.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example2.tld. NS ns.example2.tld. +ns.example2.tld. A 10.53.0.3 + +sub.example2.tld. NS ns01.sub.example2.tld. +sub.example2.tld. NS ns02.sub.example2.tld. +sub.example2.tld. NS ns03.sub.example2.tld. + +ns01.sub.example2.tld. A 10.53.1.1 +ns01.sub.example2.tld. A 10.53.0.5 +ns02.sub.example2.tld. A 10.53.1.2 +ns02.sub.example2.tld. A 10.53.0.6 +ns03.sub.example2.tld. A 10.53.2.1 +ns03.sub.example2.tld. A 10.53.0.7 diff --git a/bin/tests/system/selfpointedglue/ns3/named.conf.j2 b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 new file mode 100644 index 0000000000..b5c8bfcf33 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 @@ -0,0 +1,44 @@ +/* + * 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.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; + 10.53.0.5; + 10.53.0.6; + 10.53.0.7; + 10.53.0.8; + 10.53.0.9; + 10.53.0.10; + 10.53.1.1; + 10.53.1.2; + 10.53.2.1; + }; + recursion no; + dnssec-validation no; +}; + +zone "example.tld." { + type primary; + file "example.tld.db"; +}; + +zone "example2.tld." { + type primary; + file "example2.tld.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/named.args.j2 b/bin/tests/system/selfpointedglue/ns4/named.args.j2 new file mode 100644 index 0000000000..071508fd70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.args.j2 @@ -0,0 +1,3 @@ +{% set adblimit = adblimit | default("") %} + +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 @adblimit@ diff --git a/bin/tests/system/selfpointedglue/ns4/named.conf.j2 b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 new file mode 100644 index 0000000000..09fbdd4e70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 @@ -0,0 +1,59 @@ +/* + * 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. + */ +{% set maxdelegationservers = maxdelegationservers | default(None) %} + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; + {% if maxdelegationservers %} + @maxdelegationservers@ + {% endif %} +}; + +/* + * Forcing TCP ensures that ADDITIONAL won't be truncated (responses won't have + * the TC flag, hence the resolver won't retry using TCP by itself, see + * https://datatracker.ietf.org/doc/html/rfc2181#section-9) + */ +server 10.53.0.3 { tcp-only true; }; +server 10.53.0.5 { tcp-only true; }; +server 10.53.0.6 { tcp-only true; }; +server 10.53.0.7 { tcp-only true; }; +server 10.53.0.8 { tcp-only true; }; +server 10.53.0.9 { tcp-only true; }; +server 10.53.0.10 { tcp-only true; }; +server 10.53.1.1 { tcp-only true; }; +server 10.53.1.2 { tcp-only true; }; +server 10.53.2.1 { tcp-only true; }; + +zone "." { + type hint; + file "root.hint"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/root.hint b/bin/tests/system/selfpointedglue/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/root.hint @@ -0,0 +1,14 @@ +; 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 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.1 diff --git a/bin/tests/system/selfpointedglue/tests_selfpointedglue.py b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py new file mode 100644 index 0000000000..0fb6c3e374 --- /dev/null +++ b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py @@ -0,0 +1,122 @@ +# 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 subprocess + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_ips_and_queries(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.4:38507 -> 10.53.0.3:22047 TCP 56b sub.example.tld/IN/NS + _, _, _, _, _, dst, _, _, query = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return (ip, query) + + +def extract_dnstap(ns, expectedlen): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + assert expectedlen == len(lines) + return list(map(line_to_ips_and_queries, lines)) + + +# Because DNSTAP doesn't have ordering guarantee, the order doesn't matter here. +def expect_ip_and_query(expected_ips_and_queries, ips_and_queries): + found_count = 0 + for expected_ip, expected_query in expected_ips_and_queries: + found = False + for ip, query in ips_and_queries: + if ip == expected_ip and query == expected_query: + found = True + found_count += 1 + break + assert found + assert found_count == len(expected_ips_and_queries) + + +def test_selfpointedglue(ns4): + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 10) + + # Thanks to the de-duplication, only the first 6 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 60 + # (60 per NS, with 10 NS). + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example.tld/IN/NS"), + ("10.53.0.3", "sub.example.tld/IN/NS"), + ("10.53.0.3", "a.sub.example.tld/IN/A"), + ("10.53.0.5", "a.sub.example.tld/IN/A"), + ("10.53.0.6", "a.sub.example.tld/IN/A"), + ("10.53.0.7", "a.sub.example.tld/IN/A"), + ("10.53.0.8", "a.sub.example.tld/IN/A"), + ("10.53.0.9", "a.sub.example.tld/IN/A"), + ], + ips_and_queries, + ) + + +def test_selfpointedglue_adblimit(ns4, templates): + templates.render("ns4/named.args", {"adblimit": "-T adbaddrslimit=2"}) + with ns4.watch_log_from_here() as watcher: + # Server needs a full stop/restart to read the new command line options. + ns4.stop() + ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]]) + watcher.wait_for_line("running") + + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 6) + + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example.tld/IN/NS"), + ("10.53.0.3", "sub.example.tld/IN/NS"), + # Because of the ADB limit, only 2 IP are used instead + # of the 10 provided ones. + ("10.53.0.3", "a.sub.example.tld/IN/A"), + ("10.53.0.5", "a.sub.example.tld/IN/A"), + ], + ips_and_queries, + ) + + +def test_selfpointedglue_adblimitlower(ns4, templates): + templates.render("ns4/named.args", {"adblimit": "-T adbaddrslimit=0"}) + with ns4.watch_log_from_here() as watcher: + # Server needs a full stop/restart to read the new command line options. + ns4.stop() + failed_to_start = False + try: + ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]]) + except subprocess.CalledProcessError as _: + failed_to_start = True + assert failed_to_start is True + watcher.wait_for_line("adbaddrslimit must be at least 1") From ac3ea4ecd438c99e44f7698485df9ca30322638a Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Tue, 7 Apr 2026 22:18:58 +0200 Subject: [PATCH 13/30] rctx_resend() increment query counters Calls to `rctx_resend()` are done internally within the resolver, in flow which are not supposed to happens more than once. For instance, if some query fails, and a specific flag "F" wasn't set, then set the flag and try again. This wouldn't occur more than once because if the query fails the next attempt, the flag "F" would be set already, so the resolver would move to the next server (or give up). However, a subtle bug missing checking a flag, for instance, could lead to an unbounded loop re-trying to query the same server. This is now impossible as `rctx_resend()` also increment the query counters (so if such case occurs, it would stop once the maximum limit is reached). The dns_resstatscounter_retry are also only incremented if the `fctx_query()` succeeds, similar to as is done in `fctx_try()`. (cherry picked from commit f3e74304889a2e8b69c8e88fc9a383589decda32) --- lib/dns/resolver.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 50d781b361..6d6fb8eba3 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -9740,9 +9740,9 @@ rctx_nextserver(respctx_t *rctx, dns_message_t *message, * rctx_resend(): * * Resend the query, probably with the options changed. Calls - * fctx_query(), passing rctx->retryopts (which is based on - * query->options, but may have been updated since the last time - * fctx_query() was called). + * fctx_query(), unless query counter limits are hit, passing + * rctx->retryopts (which is based on query->options, but may have + * been updated since the last time fctx_query() was called). */ static void rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { @@ -9750,8 +9750,15 @@ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { isc_result_t result; FCTXTRACE("resend"); - inc_stats(fctx->res, dns_resstatscounter_retry); + + CHECK(incr_query_counters(fctx)); + result = fctx_query(fctx, addrinfo, rctx->retryopts); + if (result == ISC_R_SUCCESS) { + inc_stats(fctx->res, dns_resstatscounter_retry); + } + +cleanup: if (result != ISC_R_SUCCESS) { fctx_done_detach(&rctx->fctx, result); } From 8b62c25306b6f3e31f237ba7799ea4e0f15c64ae Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 14/30] Test CHAOS view recursion behavior Check that recursive and forward queries to views of type CHAOS are REFUSED, but that authoritative queries are answered correctly. (cherry picked from commit f33927cd3dd1195f3e70f5798ff7c384f265867e) --- bin/tests/system/checkconf/tests.sh | 11 ++++ .../checkconf/warn-chaos-recursion.conf | 12 +++++ bin/tests/system/class/ns1/chaos.db.in | 4 ++ bin/tests/system/class/ns1/named.conf.j2 | 31 +++++++++++ bin/tests/system/class/ns2/example.db.in | 6 +++ bin/tests/system/class/ns2/localhost.db.in | 6 +++ bin/tests/system/class/ns2/named.conf.j2 | 42 +++++++++++++++ bin/tests/system/class/ns3/named.conf.j2 | 28 ++++++++++ bin/tests/system/class/setup.sh | 19 +++++++ bin/tests/system/class/tests_class_chaos.py | 54 +++++++++++++++++++ bin/tests/system/isctest/check.py | 4 ++ 11 files changed, 217 insertions(+) create mode 100644 bin/tests/system/checkconf/warn-chaos-recursion.conf create mode 100644 bin/tests/system/class/ns1/chaos.db.in create mode 100644 bin/tests/system/class/ns1/named.conf.j2 create mode 100644 bin/tests/system/class/ns2/example.db.in create mode 100644 bin/tests/system/class/ns2/localhost.db.in create mode 100644 bin/tests/system/class/ns2/named.conf.j2 create mode 100644 bin/tests/system/class/ns3/named.conf.j2 create mode 100644 bin/tests/system/class/setup.sh create mode 100644 bin/tests/system/class/tests_class_chaos.py diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 2d7bb308c5..52666671a5 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -806,5 +806,16 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check 'recursion yes;' is warned and disabled in a non-IN view ($n)" +ret=0 +$CHECKCONF warn-chaos-recursion.conf >checkconf.out$n 2>&1 || ret=1 +grep -F "recursion will be disabled" checkconf.out$n >/dev/null || ret=1 +if [ $ret != 0 ]; then + echo_i "failed" + ret=1 +fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/checkconf/warn-chaos-recursion.conf b/bin/tests/system/checkconf/warn-chaos-recursion.conf new file mode 100644 index 0000000000..01965102a4 --- /dev/null +++ b/bin/tests/system/checkconf/warn-chaos-recursion.conf @@ -0,0 +1,12 @@ +options { + directory "."; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.hints"; + }; +}; diff --git a/bin/tests/system/class/ns1/chaos.db.in b/bin/tests/system/class/ns1/chaos.db.in new file mode 100644 index 0000000000..43ca58ffa8 --- /dev/null +++ b/bin/tests/system/class/ns1/chaos.db.in @@ -0,0 +1,4 @@ +. CH NS ns.root. +ns.root. CH A ns.root. 1 +ns.root. CH AAAA \# 1 00 + diff --git a/bin/tests/system/class/ns1/named.conf.j2 b/bin/tests/system/class/ns1/named.conf.j2 new file mode 100644 index 0000000000..76f85fc6c9 --- /dev/null +++ b/bin/tests/system/class/ns1/named.conf.j2 @@ -0,0 +1,31 @@ +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; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.db"; + }; + zone "version.bind" { + type primary; + database "_builtin version"; + }; +}; diff --git a/bin/tests/system/class/ns2/example.db.in b/bin/tests/system/class/ns2/example.db.in new file mode 100644 index 0000000000..a658ddbd89 --- /dev/null +++ b/bin/tests/system/class/ns2/example.db.in @@ -0,0 +1,6 @@ +$TTL 300 +@ CH SOA ns.example. hostmaster.example. 1 3600 1200 604800 300 +@ CH NS ns.example. +ns CH TXT "ns" +a CH A target.example. 1 +target CH TXT "target" diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in new file mode 100644 index 0000000000..baa5f74862 --- /dev/null +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -0,0 +1,6 @@ +$ORIGIN 1.0.0.127.in-addr.arpa. +$TTL 300 +@ IN SOA ns hostmaster 1 3600 900 604800 300 +@ IN NS ns +ns IN A 127.0.0.1 +@ IN KX 10 target.example. diff --git a/bin/tests/system/class/ns2/named.conf.j2 b/bin/tests/system/class/ns2/named.conf.j2 new file mode 100644 index 0000000000..5618c15216 --- /dev/null +++ b/bin/tests/system/class/ns2/named.conf.j2 @@ -0,0 +1,42 @@ +options { + directory "."; + 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; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view default { + match-clients { any; }; + recursion no; + dnssec-validation no; + zone "1.0.0.127.in-addr.arpa." { + type primary; + file "localhost.db"; + update-policy { + grant * tcp-self . ANY; + }; + }; +}; + +view chaos ch { + match-clients { any; }; + recursion no; + zone example { + type primary; + file "example.db"; + allow-update { any; }; + }; +}; diff --git a/bin/tests/system/class/ns3/named.conf.j2 b/bin/tests/system/class/ns3/named.conf.j2 new file mode 100644 index 0000000000..3016333aad --- /dev/null +++ b/bin/tests/system/class/ns3/named.conf.j2 @@ -0,0 +1,28 @@ +options { + directory "."; + 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; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + dnssec-validation no; + forward only; + forwarders port @PORT@ { 10.53.0.2; }; + deny-answer-addresses { 0.0.0.0/0; ::/0; }; +}; diff --git a/bin/tests/system/class/setup.sh b/bin/tests/system/class/setup.sh new file mode 100644 index 0000000000..c70a2f8290 --- /dev/null +++ b/bin/tests/system/class/setup.sh @@ -0,0 +1,19 @@ +#!/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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/chaos.db.in ns1/chaos.db +cp ns2/example.db.in ns2/example.db +cp ns2/localhost.db.in ns2/localhost.db diff --git a/bin/tests/system/class/tests_class_chaos.py b/bin/tests/system/class/tests_class_chaos.py new file mode 100644 index 0000000000..5b4fef9ae4 --- /dev/null +++ b/bin/tests/system/class/tests_class_chaos.py @@ -0,0 +1,54 @@ +# 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.opcode +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def test_chaos_recursion(): + msg = isctest.query.create("foo.example.", "TXT", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.refused(res) + + +def test_chaos_auth(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + + +def test_chaos_forward(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.3") + isctest.check.refused(res) + + +def test_chaos_notify(): + msg = isctest.query.create("example.", "SOA", qclass="CH", rd=False, dnssec=False) + msg.set_opcode(dns.opcode.NOTIFY) + msg.flags = dns.opcode.to_flags(dns.opcode.NOTIFY) + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.notimp(res) + + +def test_query_class_none(): + msg = isctest.query.create("example.", "A", qclass="NONE") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.formerr(res) diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index 6dbf6bca26..9b4aab5ab6 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -46,6 +46,10 @@ def servfail(message: dns.message.Message) -> None: rcode(message, dns.rcode.SERVFAIL) +def formerr(message: dns.message.Message) -> None: + rcode(message, dns.rcode.FORMERR) + + def adflag(message: dns.message.Message) -> None: assert (message.flags & dns.flags.AD) != 0, str(message) From 9bdace3fa3dd92e6b04e8d26fda8795c3b33f810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 18 Mar 2026 00:10:35 +0100 Subject: [PATCH 15/30] Fix GSS-API context leak in TKEY negotiation Reject multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) in dst_gssapi_acceptctx(). Each call to gss_accept_sec_context() allocates a context inside the GSS library; without this fix, the context handle was passed back to process_gsstkey() which did not store it persistently, leaking it on every incomplete negotiation. An unauthenticated attacker could exhaust server memory by sending repeated TKEY queries with GSSAPI tokens, each leaking one GSS context. The leaked memory is allocated by the GSS library via malloc(), bypassing BIND's memory accounting. In practice, Kerberos/SPNEGO (the only mechanism used with BIND) completes in a single round, so rejecting continuation does not affect real-world deployments. See RFC 3645 Section 4.1.3. (cherry picked from commit 3d8e0d068f08694282c5ecd3bd6c332de6c75485) --- lib/dns/gssapictx.c | 96 +++++++++++++++++++----------------- lib/dns/include/dst/gssapi.h | 17 +++---- lib/dns/tkey.c | 26 ++++------ 3 files changed, 66 insertions(+), 73 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index 3cd0fbba19..c0428609f9 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -607,7 +607,14 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, GSS_SPNEGO_MECHANISM, flags, 0, NULL, gintokenp, NULL, &gouttoken, &ret_flags, NULL); - if (gret != GSS_S_COMPLETE && gret != GSS_S_CONTINUE_NEEDED) { + switch (gret) { + case GSS_S_COMPLETE: + result = ISC_R_SUCCESS; + break; + case GSS_S_CONTINUE_NEEDED: + result = DNS_R_CONTINUE; + break; + default: gss_err_message(mctx, gret, minor, err_message); if (err_message != NULL && *err_message != NULL) { gss_log(3, "Failure initiating security context: %s", @@ -632,12 +639,6 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, CHECK(isc_buffer_copyregion(outtoken, &r)); } - if (gret == GSS_S_COMPLETE) { - result = ISC_R_SUCCESS; - } else { - result = DNS_R_CONTINUE; - } - cleanup: if (gouttoken.length != 0U) { (void)gss_release_buffer(&minor, &gouttoken); @@ -662,15 +663,10 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, char buf[1024]; REQUIRE(outtoken != NULL && *outtoken == NULL); + REQUIRE(*ctxout == NULL); REGION_TO_GBUFFER(*intoken, gintoken); - if (*ctxout == NULL) { - context = GSS_C_NO_CONTEXT; - } else { - context = *ctxout; - } - if (gssapi_keytab != NULL) { #if HAVE_GSSAPI_GSSAPI_KRB5_H || HAVE_GSSAPI_KRB5_H gret = gsskrb5_register_acceptor_identity(gssapi_keytab); @@ -715,8 +711,15 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, switch (gret) { case GSS_S_COMPLETE: - case GSS_S_CONTINUE_NEEDED: break; + /* + * RFC 3645 4.1.3: we don't handle GSS_S_CONTINUE_NEEDED + * Multi-round GSS-API negotiation is not supported. + */ + case GSS_S_CONTINUE_NEEDED: + gss_log(3, "multi-round GSS-API negotiation not supported"); + (void)gss_delete_sec_context(&minor, &context, NULL); + FALLTHROUGH; case GSS_S_DEFECTIVE_TOKEN: case GSS_S_DEFECTIVE_CREDENTIAL: case GSS_S_BAD_SIG: @@ -729,7 +732,7 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, case GSS_S_BAD_MECH: case GSS_S_FAILURE: result = DNS_R_INVALIDTKEY; - /* fall through */ + FALLTHROUGH; default: gss_log(3, "failed gss_accept_sec_context: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); @@ -747,43 +750,44 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, (void)gss_release_buffer(&minor, &gouttoken); } - if (gret == GSS_S_COMPLETE) { - gret = gss_display_name(&minor, gname, &gnamebuf, NULL); - if (gret != GSS_S_COMPLETE) { - gss_log(3, "failed gss_display_name: %s", - gss_error_tostring(gret, minor, buf, - sizeof(buf))); - CHECK(ISC_R_FAILURE); - } + INSIST(gret == GSS_S_COMPLETE); - /* - * Compensate for a bug in Solaris8's implementation - * of gss_display_name(). Should be harmless in any - * case, since principal names really should not - * contain null characters. - */ - if (gnamebuf.length > 0U && - ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') - { - gnamebuf.length--; - } - - gss_log(3, "gss-api source name (accept) is %.*s", - (int)gnamebuf.length, (char *)gnamebuf.value); - - GBUFFER_TO_REGION(gnamebuf, r); - isc_buffer_init(&namebuf, r.base, r.length); - isc_buffer_add(&namebuf, r.length); - - CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0, - NULL)); - } else { - result = DNS_R_CONTINUE; + gret = gss_display_name(&minor, gname, &gnamebuf, NULL); + if (gret != GSS_S_COMPLETE) { + gss_log(3, "failed gss_display_name: %s", + gss_error_tostring(gret, minor, buf, sizeof(buf))); + result = ISC_R_FAILURE; + goto cleanup; } + /* + * Compensate for a bug in Solaris8's implementation + * of gss_display_name(). Should be harmless in any + * case, since principal names really should not + * contain null characters. + */ + if (gnamebuf.length > 0U && + ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') + { + gnamebuf.length--; + } + + gss_log(3, "gss-api source name (accept) is %.*s", (int)gnamebuf.length, + (char *)gnamebuf.value); + + GBUFFER_TO_REGION(gnamebuf, r); + isc_buffer_init(&namebuf, r.base, r.length); + isc_buffer_add(&namebuf, r.length); + + CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0, NULL)); + *ctxout = context; cleanup: + if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) { + (void)gss_delete_sec_context(&minor, &context, NULL); + } + if (gnamebuf.length != 0U) { gret = gss_release_buffer(&minor, &gnamebuf); if (gret != GSS_S_COMPLETE) { diff --git a/lib/dns/include/dst/gssapi.h b/lib/dns/include/dst/gssapi.h index 494b4b0762..5945bf7637 100644 --- a/lib/dns/include/dst/gssapi.h +++ b/lib/dns/include/dst/gssapi.h @@ -113,20 +113,17 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, * generated by gss_accept_sec_context() to be sent to the * initiator * 'context' is a valid pointer to receive the generated context handle. - * On the initial call, it should be a pointer to NULL, which - * will be allocated as a dns_gss_ctx_id_t. Subsequent calls - * should pass in the handle generated on the first call. - * Call dst_gssapi_releasecred to delete the context and free - * the memory. * * Requires: - * 'outtoken' to != NULL && *outtoken == NULL. + * 'outtoken' != NULL && *outtoken == NULL. + * 'context' != NULL && *context == NULL. * * Returns: - * ISC_R_SUCCESS msg was successfully updated to include the - * query to be sent - * DNS_R_CONTINUE transaction still in progress - * other an error occurred while building the message + * ISC_R_SUCCESS msg was successfully updated to include + * the query to be sent + * DNS_R_INVALIDTKEY an error occurred while accepting the + * context + * ISC_R_FAILURE other error occurred */ isc_result_t diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index ecaec03d58..97b6286c25 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -190,15 +190,6 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, return ISC_R_SUCCESS; } - /* - * XXXDCL need to check for key expiry per 4.1.1 - * XXXDCL need a way to check fully established, perhaps w/key_flags - */ - result = dns_tsigkey_find(&tsigkey, name, &tkeyin->algorithm, ring); - if (result == ISC_R_SUCCESS) { - gss_ctx = dst_key_getgssctx(tsigkey->key); - } - /* * Note that tctx->gsscred may be NULL if tctx->gssapi_keytab is set */ @@ -206,7 +197,7 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, result = dst_gssapi_acceptctx(tctx->gsscred, tctx->gssapi_keytab, &intoken, &outtoken, &gss_ctx, principal, tctx->mctx); - if (result == DNS_R_INVALIDTKEY) { + if (result != ISC_R_SUCCESS) { if (tsigkey != NULL) { dns_tsigkey_detach(&tsigkey); } @@ -214,12 +205,11 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, tkey_log("process_gsstkey(): dns_tsigerror_badkey"); return ISC_R_SUCCESS; } - if (result != DNS_R_CONTINUE && result != ISC_R_SUCCESS) { - CHECK(result); - } /* - * XXXDCL Section 4.1.3: Limit GSS_S_CONTINUE_NEEDED to 10 times. + * Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is + * rejected in dst_gssapi_acceptctx(), so if we reach here the + * negotiation is complete and the principal must be set. */ if (dns_name_countlabels(principal) == 0U) { if (tsigkey != NULL) { @@ -285,6 +275,9 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, return ISC_R_SUCCESS; cleanup: + if (dstkey == NULL && gss_ctx != NULL) { + dst_gssapi_deletectx(tctx->mctx, &gss_ctx); + } if (tsigkey != NULL) { dns_tsigkey_detach(&tsigkey); } @@ -689,9 +682,8 @@ dns_tkey_gssnegotiate(dns_message_t *qmsg, dns_message_t *rmsg, NULL)); /* - * XXXSRA This seems confused. If we got CONTINUE from initctx, - * the GSS negotiation hasn't completed yet, so we can't sign - * anything yet. + * GSS negotiation is complete (CONTINUE returned earlier). + * Create the TSIG key from the established context. */ CHECK(dns_tsigkey_createfromkey(tkeyname, DST_ALG_GSSAPI, dstkey, true, false, NULL, rtkey.inception, From 2be8bdb3f44f073954daa0c5f04a9cd6dde583b1 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Mar 2026 18:25:32 +0100 Subject: [PATCH 16/30] Add SRTT-based server selection system test Verify that the resolver selects authoritative servers in increasing SRTT order. Four servers are configured with increasing response delays. 100 queries are sent, expecting most to go to the fastest server (ns2). Then ns2 stops responding, another 100 queries are sent and should go to ns3 (the next fastest), and so on through ns4 and ns5. Each query uses a unique name to avoid cache hits. (cherry picked from commit a8d11e14f5b4e4d53219ba751d1b741162b0b84b) --- bin/tests/system/srtt/README | 18 +++++ bin/tests/system/srtt/ans2/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans3/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans4/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans5/ans.py | 36 ++++++++++ bin/tests/system/srtt/ns1/named.conf.j2 | 29 ++++++++ bin/tests/system/srtt/ns1/root.db | 36 ++++++++++ bin/tests/system/srtt/ns6/named.args | 1 + bin/tests/system/srtt/ns6/named.conf.j2 | 41 ++++++++++++ bin/tests/system/srtt/srtt_ans.py | 59 ++++++++++++++++ bin/tests/system/srtt/tests_srtt.py | 89 +++++++++++++++++++++++++ 11 files changed, 417 insertions(+) create mode 100644 bin/tests/system/srtt/README create mode 100644 bin/tests/system/srtt/ans2/ans.py create mode 100644 bin/tests/system/srtt/ans3/ans.py create mode 100644 bin/tests/system/srtt/ans4/ans.py create mode 100644 bin/tests/system/srtt/ans5/ans.py create mode 100644 bin/tests/system/srtt/ns1/named.conf.j2 create mode 100644 bin/tests/system/srtt/ns1/root.db create mode 100644 bin/tests/system/srtt/ns6/named.args create mode 100644 bin/tests/system/srtt/ns6/named.conf.j2 create mode 100644 bin/tests/system/srtt/srtt_ans.py create mode 100644 bin/tests/system/srtt/tests_srtt.py diff --git a/bin/tests/system/srtt/README b/bin/tests/system/srtt/README new file mode 100644 index 0000000000..c86a697931 --- /dev/null +++ b/bin/tests/system/srtt/README @@ -0,0 +1,18 @@ +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. + +ns1 is root + +ans{2-5} simulates four NS servers making authority on the same domain +`example.`. ans2 is the quickest to answer, followed by ans3, then ans4, with +ans5 being the slowest. + +ns6 is a resolver diff --git a/bin/tests/system/srtt/ans2/ans.py b/bin/tests/system/srtt/ans2/ans.py new file mode 100644 index 0000000000..147a65f828 --- /dev/null +++ b/bin/tests/system/srtt/ans2/ans.py @@ -0,0 +1,36 @@ +""" +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.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo99Handler(DelayedQnameRangeHandler): + max_qname = 99 + delay = 0.0 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo99Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans3/ans.py b/bin/tests/system/srtt/ans3/ans.py new file mode 100644 index 0000000000..ecd590afd0 --- /dev/null +++ b/bin/tests/system/srtt/ans3/ans.py @@ -0,0 +1,36 @@ +""" +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.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo199Handler(DelayedQnameRangeHandler): + max_qname = 199 + delay = 0.03 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo199Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans4/ans.py b/bin/tests/system/srtt/ans4/ans.py new file mode 100644 index 0000000000..af337c27fb --- /dev/null +++ b/bin/tests/system/srtt/ans4/ans.py @@ -0,0 +1,36 @@ +""" +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.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo299Handler(DelayedQnameRangeHandler): + max_qname = 299 + delay = 0.08 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo299Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans5/ans.py b/bin/tests/system/srtt/ans5/ans.py new file mode 100644 index 0000000000..8bac83a798 --- /dev/null +++ b/bin/tests/system/srtt/ans5/ans.py @@ -0,0 +1,36 @@ +""" +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.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo399Handler(DelayedQnameRangeHandler): + max_qname = 399 + delay = 0.15 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo399Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ns1/named.conf.j2 b/bin/tests/system/srtt/ns1/named.conf.j2 new file mode 100644 index 0000000000..eb079c95ab --- /dev/null +++ b/bin/tests/system/srtt/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/srtt/ns1/root.db b/bin/tests/system/srtt/ns1/root.db new file mode 100644 index 0000000000..29ecd1d89d --- /dev/null +++ b/bin/tests/system/srtt/ns1/root.db @@ -0,0 +1,36 @@ +; 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 owner.root-servers.nil. 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 + +; The idea is that the resolver would do 2 ADB lookups, so there would be 2 +; find list, both with 2 IPs in it. ns1 (which is actually ans2 and ans5) would +; have both the slowest and fastest addresses. ns2 (which is actually ans3 and +; ans4) would have two addresses in the middle. + +example. NS ns1.example. +example. NS ns1.example. +example. NS ns2.example. +example. NS ns2.example. + +ns1.example. A 10.53.0.2 ; delay is 0 +ns1.example. A 10.53.0.5 ; delay is 0.15 +ns2.example. A 10.53.0.4 ; delay is 0.08 +ns2.example. A 10.53.0.3 ; delay is 0.03 diff --git a/bin/tests/system/srtt/ns6/named.args b/bin/tests/system/srtt/ns6/named.args new file mode 100644 index 0000000000..b5de5874ec --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.args @@ -0,0 +1 @@ +-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/srtt/ns6/named.conf.j2 b/bin/tests/system/srtt/ns6/named.conf.j2 new file mode 100644 index 0000000000..1d27505a8e --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.conf.j2 @@ -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 { + query-source address 10.53.0.6; + notify-source 10.53.0.6; + transfer-source 10.53.0.6; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.6; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.6 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "." { + type hint; + file "../../_common/root.hint"; +}; diff --git a/bin/tests/system/srtt/srtt_ans.py b/bin/tests/system/srtt/srtt_ans.py new file mode 100644 index 0000000000..9387486993 --- /dev/null +++ b/bin/tests/system/srtt/srtt_ans.py @@ -0,0 +1,59 @@ +""" +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. +""" + +from collections.abc import AsyncGenerator + +import abc + +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import DnsResponseSend, QnameQtypeHandler, QueryContext + + +class DelayedQnameRangeHandler(QnameQtypeHandler): + """ + Respond to queries for QNAMEs "foo1.example." through "foo.example." + with QTYPE=A, where must be defined by the subclass. Every response is + delayed by a fixed amount of time, which must also be defined (in seconds) + by the subclass. + """ + + @property + def qnames(self) -> list[str]: + return [f"foo{x}.example." for x in range(1, self.max_qname + 1)] + + qtypes = [dns.rdatatype.A] + + @property + @abc.abstractmethod + def max_qname(self) -> int: + raise NotImplementedError + + @property + @abc.abstractmethod + def delay(self) -> float: + raise NotImplementedError + + def __str__(self) -> str: + return f"{self.__class__.__name__}(foo[1-{self.max_qname}].example/A)" + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + a_rrset = dns.rrset.from_text( + qctx.qname, 300, dns.rdataclass.IN, dns.rdatatype.A, "10.53.9.9" + ) + qctx.response.answer.append(a_rrset) + yield DnsResponseSend(qctx.response, delay=self.delay) diff --git a/bin/tests/system/srtt/tests_srtt.py b/bin/tests/system/srtt/tests_srtt.py new file mode 100644 index 0000000000..0ce18fcccf --- /dev/null +++ b/bin/tests/system/srtt/tests_srtt.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. + +import os + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_dst_ips(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.6:38507 -> 10.53.0.3:22047 TCP 56b fooXXX.example./IN/NS + _, _, _, _, _, dst, _, _, _ = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return ip + + +def extract_dnstap(ns): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + return map(line_to_dst_ips, lines) + + +def assert_used_auth(ns, authip): + ips = extract_dnstap(ns) + queries = 0 + matches = 0 + for ip in ips: + queries += 1 + if ip == authip: + matches += 1 + assert matches > 85 + assert queries <= 115 + + +def test_srtt(ns6): + for i in range(1, 100): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.2") + + for i in range(100, 200): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.3") + + for i in range(200, 300): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.4") + + for i in range(300, 400): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + assert_used_auth(ns6, "10.53.0.5") From aecc27189f6288237a0559b6fad2dcd09f1fb1e2 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 17/30] Test UPDATE behavior in CHAOS and other non-IN classes Send various UPDATE requests that are known to have caused crashes previously with deliberately misconfigured non-IN zones; confirm that UPDATE is not processed. (cherry picked from commit e2f7ba2a4b6e7e5dba2fb1a2c9b2f0323e9a88be) --- bin/named/server.c | 1 - bin/tests/system/class/ns2/localhost.db.in | 5 + bin/tests/system/class/tests_class_update.py | 96 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 bin/tests/system/class/tests_class_update.py diff --git a/bin/named/server.c b/bin/named/server.c index 63bb311d3a..df4bb6ff4f 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -5392,7 +5392,6 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, &view->proxyonacl)); if (view->rdclass != dns_rdataclass_in) { - view->recursion = false; dns_acl_none(named_g_mctx, &view->recursionacl); dns_acl_none(named_g_mctx, &view->recursiononacl); } else { diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in index baa5f74862..a50e5167a9 100644 --- a/bin/tests/system/class/ns2/localhost.db.in +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -3,4 +3,9 @@ $TTL 300 @ IN SOA ns hostmaster 1 3600 900 604800 300 @ IN NS ns ns IN A 127.0.0.1 + @ IN KX 10 target.example. +@ IN PX 10 map822.example. mapx400.example. +@ IN NSAP 0x47000580ffff0000000001e133ffffff00016200 +@ IN NSAP-PTR target.example. +@ in EID \# 01 aa diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py new file mode 100644 index 0000000000..e53bbc77ea --- /dev/null +++ b/bin/tests/system/class/tests_class_update.py @@ -0,0 +1,96 @@ +# 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 socket +import struct + +from dns import rdataclass, rdatatype, update + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def encode_name(name: str) -> bytes: + out = b"" + for label in name.rstrip(".").split("."): + out += bytes([len(label)]) + label.encode("ascii") + return out + b"\x00" + + +@pytest.mark.parametrize( + "rdtype,rdclass,ttl,rdata", + [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), + (rdatatype.KX, rdataclass.NONE, 0, b""), + (rdatatype.PX, rdataclass.NONE, 0, b""), + (rdatatype.NSAP, rdataclass.NONE, 0, b""), + (rdatatype.NSAP_PTR, rdataclass.NONE, 0, b""), + (31, rdataclass.NONE, 0, b""), # dnspython doesn't define type EID + ], +) +def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): + # these update messages are badly formatted, so we construct + # them manually instead of using dnspython. + + # opcode=UPDATE, 1 RRset in ZONE, 1 RRset in UPDATE + header = struct.pack("!HHHHHH", 0, 0x2800, 1, 0, 1, 0) + + # ZONE section: QNAME=, QTYPE=SOA, QCLASS=ANY + zone_q = encode_name("1.0.0.127.in-addr.arpa") + struct.pack("!HH", 6, 255) + + # UPDATE section RR: + update_rr = ( + encode_name("1.0.0.127.in-addr.arpa") + + struct.pack("!HHIH", rdtype, rdclass, ttl, len(rdata)) + + rdata + ) + + m = header + zone_q + update_rr + packet = struct.pack("!H", len(m)) + m + + with socket.create_connection( + ("10.53.0.2", named_port), source_address=("127.0.0.1", 0), timeout=2.0 + ) as s: + s.sendall(packet) + try: + rwire = s.recv(4096) + res = dns.message.from_wire(rwire) + isctest.check.formerr(res) + except Exception: # pylint: disable=broad-except + pass + + # check the server is answering + msg = isctest.query.create("1.0.0.127.in-addr.arpa", "SRV") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 0) + + +@pytest.mark.parametrize( + "rdtype,rdata", + [ + (rdatatype.SVCB, "\\# 02 0000"), + (rdatatype.WKS, "\\# 02 4142"), + (rdatatype.WKS, "\\# 02 4344"), + ], +) +def test_class_chaosupdate(rdtype, rdata): + up = update.UpdateMessage("example.", rdclass=rdataclass.CHAOS) + up.add("foo.example.", 300, rdtype, rdata) + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) From 9367f7037ce08d266ec14027844550c0f1dac8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Fri, 20 Mar 2026 08:43:28 +0100 Subject: [PATCH 18/30] Add regression test for GSS-API context leak via TKEY CONTINUE Send crafted SPNEGO NegTokenInit tokens that propose the krb5 mechanism without a mechToken. This causes gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED, which on unfixed code leaks the GSS context handle (~520 bytes per query). The test verifies that the server rejects the negotiation (TKEY error != 0, no continuation token) rather than returning a CONTINUE response (error=0 with output token). (cherry picked from commit 2f2fb32d737e12c817880d584145cdf85dbc8d06) --- bin/tests/system/tkeyleak/ns1/dns.keytab | Bin 0 -> 460 bytes bin/tests/system/tkeyleak/ns1/example.db.in | 21 +++ bin/tests/system/tkeyleak/ns1/named.conf.j2 | 39 ++++++ bin/tests/system/tkeyleak/prereq.sh | 21 +++ bin/tests/system/tkeyleak/setup.sh | 17 +++ bin/tests/system/tkeyleak/tests_tkeyleak.py | 145 ++++++++++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 bin/tests/system/tkeyleak/ns1/dns.keytab create mode 100644 bin/tests/system/tkeyleak/ns1/example.db.in create mode 100644 bin/tests/system/tkeyleak/ns1/named.conf.j2 create mode 100644 bin/tests/system/tkeyleak/prereq.sh create mode 100644 bin/tests/system/tkeyleak/setup.sh create mode 100644 bin/tests/system/tkeyleak/tests_tkeyleak.py diff --git a/bin/tests/system/tkeyleak/ns1/dns.keytab b/bin/tests/system/tkeyleak/ns1/dns.keytab new file mode 100644 index 0000000000000000000000000000000000000000..d5a09b060a2077c465dfcbbf36bc37cb241eeb6a GIT binary patch literal 460 zcmZQ&VqjnhVqjw6c8zfK4e)W*^Yip!V0Q5fX5db(NX#wBN!82C%mFH5O!{Ltm5D)! zLBWW7Wq*p(GKZ&v?D(;ENo!OT)C zNV&EkN#%}B;vXTXDPdSm;ZMpb)x+h!Sjm(%N0KtMg#IdAagny#v{(CG*3>RnR}%$^ z=n1NfdQg+yNHiJf#>HEN)mOiq9=t=C@8{&ie`ZjVJQ)~K!;d{BuUHr8M4&t(*it-a z3ssr9E&s6I>~HYOyUGuIF0&|f8@_sXj5jZ%_QN!&VU~mq1G;39!KSA+ENvPE++H3( KuWqpfX$Alz=7X02 literal 0 HcmV?d00001 diff --git a/bin/tests/system/tkeyleak/ns1/example.db.in b/bin/tests/system/tkeyleak/ns1/example.db.in new file mode 100644 index 0000000000..dd200dc9bc --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/example.db.in @@ -0,0 +1,21 @@ +; 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 ns.example. admin.example. ( + 1 ; serial + 3600 ; refresh + 900 ; retry + 604800 ; expire + 300 ; minimum + ) +@ IN NS ns.example. +ns IN A 10.53.0.1 diff --git a/bin/tests/system/tkeyleak/ns1/named.conf.j2 b/bin/tests/system/tkeyleak/ns1/named.conf.j2 new file mode 100644 index 0000000000..f16b53414c --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/named.conf.j2 @@ -0,0 +1,39 @@ +/* + * 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; + dnssec-validation no; + tkey-gssapi-keytab "dns.keytab"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "example" { + type primary; + file "example.db"; +}; diff --git a/bin/tests/system/tkeyleak/prereq.sh b/bin/tests/system/tkeyleak/prereq.sh new file mode 100644 index 0000000000..8a68ae7df1 --- /dev/null +++ b/bin/tests/system/tkeyleak/prereq.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# 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 + +$FEATURETEST --gssapi || { + echo_i "gssapi not supported - skipping tkeyleak test" + exit 255 +} + +exit 0 diff --git a/bin/tests/system/tkeyleak/setup.sh b/bin/tests/system/tkeyleak/setup.sh new file mode 100644 index 0000000000..24a0026665 --- /dev/null +++ b/bin/tests/system/tkeyleak/setup.sh @@ -0,0 +1,17 @@ +#!/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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/example.db.in ns1/example.db diff --git a/bin/tests/system/tkeyleak/tests_tkeyleak.py b/bin/tests/system/tkeyleak/tests_tkeyleak.py new file mode 100644 index 0000000000..fd97c8540c --- /dev/null +++ b/bin/tests/system/tkeyleak/tests_tkeyleak.py @@ -0,0 +1,145 @@ +# 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 GSS-API context leak via repeated TKEY queries. + +An unauthenticated attacker could exhaust server memory by sending +repeated TKEY queries with crafted SPNEGO NegTokenInit tokens. +Each query triggers gss_accept_sec_context() which returns +GSS_S_CONTINUE_NEEDED and allocates a GSS context. On the unfixed +code path, the context handle in process_gsstkey() is never stored +or freed, leaking ~520 bytes per query. + +The fix rejects GSS_S_CONTINUE_NEEDED in dst_gssapi_acceptctx() and +deletes the context immediately. + +The key distinguishing signal in the TKEY response: + - CONTINUE (vulnerable): error=0, output token present, no TSIG + - BADKEY (fixed): error=17, no output token +""" + +import struct +import time + +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.TKEY +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + +TKEY_NAME = dns.name.from_text("test.key.") +GSSAPI_ALGORITHM = dns.name.from_text("gss-tsig.") +TKEY_MODE_GSSAPI = 3 + +# OID 1.2.840.113554.1.2.2 (Kerberos 5) +KRB5_OID = b"\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" + +# OID 1.3.6.1.5.5.2 (SPNEGO) +SPNEGO_OID = b"\x06\x06\x2b\x06\x01\x05\x05\x02" + + +def der_encode(tag, data): + """Encode data in ASN.1 DER TLV format.""" + length = len(data) + if length < 128: + return tag + bytes([length]) + data + if length < 256: + return tag + b"\x81" + bytes([length]) + data + return tag + b"\x82" + struct.pack(">H", length) + data + + +def spnego_negtokeninit(): + """Build a SPNEGO NegTokenInit proposing krb5 without a mechToken. + + This forces gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED + because the acceptor recognizes the krb5 mechanism but has not + received an actual AP-REQ token yet. + """ + # MechTypeList ::= SEQUENCE OF MechType + mechtype_list = der_encode(b"\x30", KRB5_OID) + # [0] mechTypes + mechtypes = der_encode(b"\xa0", mechtype_list) + # NegTokenInit ::= SEQUENCE { mechTypes, ... } + negtokeninit = der_encode(b"\x30", mechtypes) + # [0] CONSTRUCTED (wrapping NegTokenInit) + wrapped = der_encode(b"\xa0", negtokeninit) + # APPLICATION 0 CONSTRUCTED (SPNEGO OID + body) + return der_encode(b"\x60", SPNEGO_OID + wrapped) + + +def make_tkey_query(token): + """Build a TKEY query with a GSS-API token in the additional section.""" + now = int(time.time()) + tkey_rdata = dns.rdtypes.ANY.TKEY.TKEY( + rdclass=dns.rdataclass.ANY, + rdtype=dns.rdatatype.TKEY, + algorithm=GSSAPI_ALGORITHM, + inception=now, + expiration=now + 86400, + mode=TKEY_MODE_GSSAPI, + error=0, + key=token, + other=b"", + ) + + msg = isctest.query.create(TKEY_NAME, dns.rdatatype.TKEY, dns.rdataclass.ANY) + rrset = msg.find_rrset( + msg.additional, + TKEY_NAME, + dns.rdataclass.ANY, + dns.rdatatype.TKEY, + create=True, + ) + rrset.add(tkey_rdata) + return msg + + +def test_tkey_gssapi_no_continuation(ns1): + """TKEY with a SPNEGO NegTokenInit must be rejected, not continued. + + On unfixed code, gss_accept_sec_context() returns CONTINUE_NEEDED + and the response has error=0 with an output token (the leaked path). + On fixed code, CONTINUE_NEEDED is rejected and the response has + error=BADKEY(17) with no output token. + """ + port = ns1.ports.dns + ip = ns1.ip + + msg = make_tkey_query(spnego_negtokeninit()) + res = dns.query.tcp(msg, ip, port=port, timeout=5) + + assert res is not None + + tkey = get_tkey_answer(res) + assert tkey is not None, "server did not return a TKEY answer" + assert ( + tkey.error != 0 + ), "server returned error=0 (GSS_S_CONTINUE_NEEDED not rejected)" + assert len(tkey.key) == 0, "server returned a continuation token" + + +def get_tkey_answer(response): + """Extract TKEY rdata from a DNS response, or None.""" + for rrset in response.answer: + if rrset.rdtype == dns.rdatatype.TKEY: + for rdata in rrset: + return rdata + return None From 1222a2aa0522ce97b2f8749f8628b5aee1461ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 17 Mar 2026 04:45:16 +0100 Subject: [PATCH 19/30] Fix use-after-free in resolver SIG(0) async verification path When a SIG(0)-signed response triggers async ECDSA verification via dns_message_checksig_async(), the respctx_t holds a raw pointer to the resquery_t. If the fetch context is shut down while verification is in flight (e.g. due to recursive-clients quota exhaustion), the query is destroyed and the callback dereferences a dangling pointer. Take a reference on the resquery_t when initializing the respctx_t, and release it in both cleanup paths. The query's own reference to the fetch context keeps the fctx alive transitively. (cherry picked from commit 5b58caf5a2cd39d57a51b7b0373bfbc4877a96f9) --- lib/dns/resolver.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 8b76b7da23..87f423a69b 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -7632,6 +7632,7 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { return; cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -7981,6 +7982,7 @@ resquery_response_continue(void *arg, isc_result_t result) { rctx_done(rctx, result); cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -7994,7 +7996,7 @@ static void rctx_respinit(resquery_t *query, fetchctx_t *fctx, isc_result_t result, isc_region_t *region, respctx_t *rctx) { *rctx = (respctx_t){ .result = result, - .query = query, + .query = resquery_ref(query), .fctx = fctx, .broken_type = badns_response, .retryopts = query->options }; From 14f755029d2922ac37be10e0112a52dd4841bbb8 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Mon, 9 Mar 2026 15:50:04 +1100 Subject: [PATCH 20/30] Test server behavior when sending various UPDATE requests Send update messages for zones with CLASS0, ANY and NONE. The class ANY UPDATE also attempts to delete a KX record in an existing IN class zone to trigger a REQUIRE. Test that the server is still running. (cherry picked from commit 1fa1e84d286d5a6d9d3b72ed1c2c29142f40c81d) --- bin/tests/system/class/tests_class_update.py | 45 +++++++++++++++++++- bin/tests/system/nsupdate/setup.sh | 1 + bin/tests/system/nsupdate/tests.sh | 20 +++------ bin/tests/system/packet.pl | 25 +++++++++-- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py index e53bbc77ea..30e3ba6d2a 100644 --- a/bin/tests/system/class/tests_class_update.py +++ b/bin/tests/system/class/tests_class_update.py @@ -12,7 +12,7 @@ import socket import struct -from dns import rdataclass, rdatatype, update +from dns import message, rdataclass, rdatatype, update import pytest @@ -35,6 +35,7 @@ def encode_name(name: str) -> bytes: @pytest.mark.parametrize( "rdtype,rdclass,ttl,rdata", [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00\x00\x00\x00\x00\x00\x01"), (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), (rdatatype.KX, rdataclass.NONE, 0, b""), (rdatatype.PX, rdataclass.NONE, 0, b""), @@ -69,7 +70,7 @@ def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): s.sendall(packet) try: rwire = s.recv(4096) - res = dns.message.from_wire(rwire) + res = message.from_wire(rwire) isctest.check.formerr(res) except Exception: # pylint: disable=broad-except pass @@ -94,3 +95,43 @@ def test_class_chaosupdate(rdtype, rdata): up.add("foo.example.", 300, rdtype, rdata) res = isctest.query.tcp(up, "10.53.0.2") isctest.check.notimp(res) + + +def test_class_undefined(ns2): + up = update.UpdateMessage(".", rdclass=257) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) + watcher.wait_for_line("invalid message class: CLASS257") + + +def test_class_zero(ns2): + up = update.UpdateMessage(".", rdclass=0) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message class could not be determined") + + +def test_class_any(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.ANY) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") + + +def test_class_none(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.NONE) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh index 299330773a..9d27b20a76 100644 --- a/bin/tests/system/nsupdate/setup.sh +++ b/bin/tests/system/nsupdate/setup.sh @@ -35,6 +35,7 @@ update.nil IN SOA ns1.example.nil. hostmaster.example.nil. ( 3600 ; minimum (1 hour) ) update.nil. NS ns1.update.nil. +update.nil. KX 0 . ns1.update.nil. A 10.53.0.2 ns2.update.nil. AAAA ::1 EOF diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh index e7c6283569..862ebe38b3 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -340,8 +340,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 update is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000100000000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { @@ -352,20 +354,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 additional data is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000000010000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 -$DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 -grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 -[ $ret = 0 ] || { - echo_i "failed" - status=1 -} - -n=$((n + 1)) -ret=0 -echo_i "check that update to undefined class is handled ($n)" -echo "a0e4280000010001000000000000060101c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { diff --git a/bin/tests/system/packet.pl b/bin/tests/system/packet.pl index 900a0c071e..afb9f4784d 100644 --- a/bin/tests/system/packet.pl +++ b/bin/tests/system/packet.pl @@ -40,6 +40,7 @@ # -p : specify port # -t : specify UDP or TCP # -r : send packet times +# -b: blocking io # -d: dump response packets # # If not specified, address defaults to 127.0.0.1, port to 53, protocol @@ -51,6 +52,8 @@ use strict; use Getopt::Std; use IO::File; use IO::Socket; +use Net::DNS; +use Net::DNS::Packet; sub usage { print ("Usage: packet.pl [-a address] [-d] [-p port] [-t (tcp|udp)] [-r ] [file]\n"); @@ -61,8 +64,6 @@ my $sock; my $proto; sub dumppacket { - use Net::DNS; - use Net::DNS::Packet; my $rin; my $rout; @@ -96,7 +97,7 @@ sub dumppacket { } my %options={}; -getopts("a:dp:t:r:", \%options); +getopts("a:bdp:t:r:", \%options); my $addr = "127.0.0.1"; $addr = $options{a} if defined $options{a}; @@ -111,6 +112,8 @@ usage if ($proto !~ /^(udp|tcp)$/); my $repeats = 1; $repeats = $options{r} if defined $options{r}; +my $blocking = defined $options{b} ? 1 : 0; + my $file = "STDIN"; if (@ARGV >= 1) { my $filename = shift @ARGV; @@ -132,8 +135,22 @@ my $len = length $data; my $output = unpack("H*", $data); print ("sending $repeats time(s): $output\n"); + +if (defined $options{d}) { + my $request; + if ($Net::DNS::VERSION > 0.68) { + $request = new Net::DNS::Packet(\$data, 0); + $@ and die $@; + } else { + my $err; + ($request, $err) = new Net::DNS::Packet(\$data, 0); + $err and die $err; + } + $request->print; +} + $sock = IO::Socket::INET->new(PeerAddr => $addr, PeerPort => $port, - Blocking => 0, + Blocking => $blocking, Proto => $proto,) or die "$!"; STDOUT->autoflush(1); From 11276087a74f0c79885524ea913fcee3d759324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Fri, 10 Apr 2026 12:51:31 +0200 Subject: [PATCH 21/30] Fix output token and GSS context leaks in TKEY/GSS-API error paths In dst_gssapi_acceptctx(), rename outtoken to outtokenp (matching BIND convention for output pointer parameters) and free the allocated output token buffer on error in the cleanup path. In process_gsstkey(), route the empty-principal error path through cleanup via CLEANUP() instead of returning early, so that the output token, GSS context, and TSIG key are all freed consistently by the existing cleanup block. (cherry picked from commit 6c46c85d02849fb659584275313529794039f433) --- lib/dns/gssapictx.c | 12 ++++++++---- lib/dns/tkey.c | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index c0428609f9..ca6587befe 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -649,7 +649,7 @@ cleanup: isc_result_t dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, - isc_region_t *intoken, isc_buffer_t **outtoken, + isc_region_t *intoken, isc_buffer_t **outtokenp, dns_gss_ctx_id_t *ctxout, dns_name_t *principal, isc_mem_t *mctx) { isc_region_t r; @@ -662,7 +662,7 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, isc_result_t result; char buf[1024]; - REQUIRE(outtoken != NULL && *outtoken == NULL); + REQUIRE(outtokenp != NULL && *outtokenp == NULL); REQUIRE(*ctxout == NULL); REGION_TO_GBUFFER(*intoken, gintoken); @@ -743,10 +743,10 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, } if (gouttoken.length > 0U) { - isc_buffer_allocate(mctx, outtoken, + isc_buffer_allocate(mctx, outtokenp, (unsigned int)gouttoken.length); GBUFFER_TO_REGION(gouttoken, r); - CHECK(isc_buffer_copyregion(*outtoken, &r)); + CHECK(isc_buffer_copyregion(*outtokenp, &r)); (void)gss_release_buffer(&minor, &gouttoken); } @@ -784,6 +784,10 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, *ctxout = context; cleanup: + if (result != ISC_R_SUCCESS && *outtokenp != NULL) { + isc_buffer_free(outtokenp); + } + if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) { (void)gss_delete_sec_context(&minor, &context, NULL); } diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index 97b6286c25..6cdac8ccdb 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -198,12 +198,10 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, &intoken, &outtoken, &gss_ctx, principal, tctx->mctx); if (result != ISC_R_SUCCESS) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } tkeyout->error = dns_tsigerror_badkey; tkey_log("process_gsstkey(): dns_tsigerror_badkey"); - return ISC_R_SUCCESS; + result = ISC_R_SUCCESS; + goto cleanup; } /* @@ -212,9 +210,11 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, * negotiation is complete and the principal must be set. */ if (dns_name_countlabels(principal) == 0U) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } + tkeyout->error = dns_tsigerror_badkey; + tkey_log("process_gsstkey(): " + "completed context with empty principal"); + result = ISC_R_SUCCESS; + goto cleanup; } else if (tsigkey == NULL) { #if HAVE_GSSAPI OM_uint32 gret, minor, lifetime; @@ -288,7 +288,9 @@ cleanup: isc_buffer_free(&outtoken); } - tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + if (result != ISC_R_SUCCESS) { + tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + } return result; } From a2d74e7356aaf56cbab2d074b47c3d896f381ad3 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Mon, 23 Feb 2026 16:27:52 -0800 Subject: [PATCH 22/30] Make the RD flag optional in isctest.query() Add an 'rd' parameter (default True) to isctest.query.create() so that non-recursive queries can be sent with rd=False. (cherry picked from commit 12e511310024aac38ce223ee47b5108f06caf8f9) --- bin/tests/system/isctest/query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/tests/system/isctest/query.py b/bin/tests/system/isctest/query.py index 97ebb4bc26..a7e862b7f6 100644 --- a/bin/tests/system/isctest/query.py +++ b/bin/tests/system/isctest/query.py @@ -136,6 +136,7 @@ def create( qtype, qclass=dns.rdataclass.IN, dnssec: bool = True, + rd: bool = True, cd: bool = False, ad: bool = True, ) -> dns.message.Message: @@ -143,7 +144,9 @@ def create( msg = dns.message.make_query( qname, qtype, qclass, use_edns=True, want_dnssec=dnssec ) - msg.flags = dns.flags.RD + msg.flags = 0 + if rd: + msg.flags = dns.flags.RD if ad: msg.flags |= dns.flags.AD if cd: From a083b4b3cf94af1e48b8e15a6af43148359f9b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= Date: Mon, 9 Mar 2026 15:48:34 +0300 Subject: [PATCH 23/30] Add system test for HTTP/2 SETTINGS frame flood Send a valid DoH query followed by a flood of SETTINGS frames to trigger a use-after-free in the write buffer. Under ASan, named will abort if the bug is present. (cherry picked from commit 680e57c8c7110828da22cbc94115a10e8dec2523) --- bin/tests/system/doth/tests_malicious.py | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 bin/tests/system/doth/tests_malicious.py diff --git a/bin/tests/system/doth/tests_malicious.py b/bin/tests/system/doth/tests_malicious.py new file mode 100644 index 0000000000..7529f2b7e1 --- /dev/null +++ b/bin/tests/system/doth/tests_malicious.py @@ -0,0 +1,73 @@ +# 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 socket +import ssl + +from h2.config import H2Configuration +from h2.connection import H2Connection +from h2.settings import SettingCodes + +import dns.message + + +def test_settings_frame_flood(ns1, named_httpsport): + msg = dns.message.make_query(".", "SOA") + wire = msg.to_wire() + + with socket.create_connection((ns1.ip, named_httpsport), timeout=10) as sock: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.set_alpn_protocols(["h2"]) + + with ctx.wrap_socket(sock, server_hostname=ns1.ip) as tls: + config = H2Configuration(client_side=True, header_encoding="utf-8") + conn = H2Connection(config=config) + conn.initiate_connection() + tls.sendall(conn.data_to_send()) + + stream_id = conn.get_next_available_stream_id() + conn.send_headers( + stream_id, + [ + (":method", "POST"), + (":path", "/dns-query"), + (":scheme", "https"), + (":authority", f"{ns1.ip}:{named_httpsport}"), + ("content-type", "application/dns-message"), + ("accept", "application/dns-message"), + ("content-length", str(len(wire))), + ], + ) + conn.send_data(stream_id, wire, end_stream=True) + tls.sendall(conn.data_to_send()) + + for i in range(4096): + try: + conn.update_settings( + { + SettingCodes.MAX_CONCURRENT_STREAMS: (i % 100) + 1, + SettingCodes.INITIAL_WINDOW_SIZE: i + 1, + } + ) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + break + + if i % 500 == 0: + tls.settimeout(0.05) + try: + while (data := tls.recv(65535)) != b"": + conn.receive_data(data) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + pass From ddd5586a519dca348e7a454885f674e2ba49db42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Sun, 19 Apr 2026 21:36:43 +0200 Subject: [PATCH 24/30] Make isc_mem_isovermem() probabilistic Replace the hysteretic hi_water/lo_water switch with a stochastic check: always false below lo_water, always true at or above hi_water, linearly ramped probability in between. This spreads cache cleaning across many inserts instead of triggering a thundering herd once the hi_water mark is crossed (which causes every addrdataset to enter the LRU purge path simultaneously and serializes lookups behind the node write locks). The is_overmem atomic and its stores are no longer needed and are removed. The existing tests that asserted specific hysteretic state transitions are simplified to check only the deterministic boundaries. (cherry picked from commit ee24d2a1c3361dcc1c48fb29bb2e0b91bc3405e8) --- lib/isc/mem.c | 65 +++++++++++++++---------------------------- tests/dns/qpdb_test.c | 2 -- tests/isc/mem_test.c | 27 ++++++++++++------ 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/lib/isc/mem.c b/lib/isc/mem.c index 5d00eb6154..12136fc366 100644 --- a/lib/isc/mem.c +++ b/lib/isc/mem.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -131,7 +132,6 @@ struct isc_mem { char name[16]; atomic_size_t inuse; atomic_bool hi_called; - atomic_bool is_overmem; atomic_size_t hi_water; atomic_size_t lo_water; ISC_LIST(isc_mempool_t) pools; @@ -570,7 +570,6 @@ mem_create(isc_mem_t **ctxp, unsigned int debugging, unsigned int flags, atomic_init(&ctx->hi_water, 0); atomic_init(&ctx->lo_water, 0); atomic_init(&ctx->hi_called, false); - atomic_init(&ctx->is_overmem, false); ISC_LIST_INIT(ctx->pools); @@ -1017,48 +1016,30 @@ bool isc_mem_isovermem(isc_mem_t *ctx) { REQUIRE(VALID_CONTEXT(ctx)); - bool is_overmem = atomic_load_relaxed(&ctx->is_overmem); - - if (!is_overmem) { - /* We are not overmem, check whether we should be? */ - size_t hiwater = atomic_load_relaxed(&ctx->hi_water); - if (hiwater == 0) { - return false; - } - - size_t inuse = atomic_load_relaxed(&ctx->inuse); - if (inuse <= hiwater) { - return false; - } - - if ((isc_mem_debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem mctx %p inuse %zu hi_water %zu\n", ctx, - inuse, hiwater); - } - - atomic_store_relaxed(&ctx->is_overmem, true); - return true; - } else { - /* We are overmem, check whether we should not be? */ - size_t lowater = atomic_load_relaxed(&ctx->lo_water); - if (lowater == 0) { - return false; - } - - size_t inuse = atomic_load_relaxed(&ctx->inuse); - if (inuse >= lowater) { - return true; - } - - if ((isc_mem_debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem mctx %p inuse %zu lo_water %zu\n", ctx, - inuse, lowater); - } - atomic_store_relaxed(&ctx->is_overmem, false); + size_t hiwater = atomic_load_relaxed(&ctx->hi_water); + if (hiwater == 0) { return false; } + + size_t inuse = atomic_load_relaxed(&ctx->inuse); + if (inuse >= hiwater) { + return true; + } + + size_t lowater = atomic_load_relaxed(&ctx->lo_water); + if (inuse <= lowater) { + return false; + } + + /* + * Between lo_water and hi_water, return true with a probability + * that ramps linearly from 0 at lo_water to 1 at hi_water. This + * spreads cache cleaning across many inserts instead of triggering + * a thundering herd once the hi_water mark is crossed. + */ + uint32_t prob = (uint32_t)(((uint64_t)(inuse - lowater) * 256) / + (hiwater - lowater)); + return isc_random8() < prob; } void diff --git a/tests/dns/qpdb_test.c b/tests/dns/qpdb_test.c index c1888101d1..260b6c1dc3 100644 --- a/tests/dns/qpdb_test.c +++ b/tests/dns/qpdb_test.c @@ -137,7 +137,6 @@ ISC_LOOP_TEST_IMPL(overmempurge_bigrdata) { for (i = 0; !isc_mem_isovermem(mctx2) && i < (maxcache / 10); i++) { overmempurge_addrdataset(db, now, i, 50053, 0, false); } - assert_true(isc_mem_isovermem(mctx2)); /* * Then try to add the same number of entries, each has very large data. @@ -188,7 +187,6 @@ ISC_LOOP_TEST_IMPL(overmempurge_longname) { for (i = 0; !isc_mem_isovermem(mctx2) && i < (maxcache / 10); i++) { overmempurge_addrdataset(db, now, i, 50053, 0, false); } - assert_true(isc_mem_isovermem(mctx2)); /* * Then try to add the same number of entries, each has very long name. diff --git a/tests/isc/mem_test.c b/tests/isc/mem_test.c index 7754488fb0..2c26922c8d 100644 --- a/tests/isc/mem_test.c +++ b/tests/isc/mem_test.c @@ -291,6 +291,17 @@ ISC_RUN_TEST_IMPL(isc_mem_reallocate) { isc_mem_free(mctx, data); } +static bool +at_least_one_overmem(isc_mem_t *omctx) { + for (size_t i = 0; i < UINT16_MAX; i++) { + /* The overmem is probability based in this range */ + if (isc_mem_isovermem(omctx)) { + return true; + } + } + return false; +} + ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_t *omctx = NULL; isc_mem_create(&omctx); @@ -298,27 +309,27 @@ ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_setwater(omctx, 1024, 512); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ void *data1 = isc_mem_allocate(omctx, 256); assert_false(isc_mem_isovermem(omctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ void *data2 = isc_mem_allocate(omctx, 512); - assert_false(isc_mem_isovermem(omctx)); + assert_true(at_least_one_overmem(omctx)); - /* hi_water < inuse */ + /* hi_water <= inuse is always true */ void *data3 = isc_mem_allocate(omctx, 512); assert_true(isc_mem_isovermem(omctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ isc_mem_free(omctx, data2); - assert_true(isc_mem_isovermem(omctx)); + assert_true(at_least_one_overmem(omctx)); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ isc_mem_free(omctx, data3); assert_false(isc_mem_isovermem(omctx)); - /* inuse == 0 */ + /* inuse == 0 is always false */ isc_mem_free(omctx, data1); assert_false(isc_mem_isovermem(omctx)); From 637a127f6506db4260584ce3840fb4a6fde9112a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= Date: Tue, 10 Mar 2026 14:48:02 +0300 Subject: [PATCH 25/30] Fix use-after-free in DoH write buffer after HTTP/2 send After the send callback completes, the UV request is freed but the HTTP/2 socket's write buffer still points to the freed memory. If nghttp2 subsequently needs to send frames (e.g. SETTINGS ACK), the server_read_callback reads from the dangling buffer. Clear the write buffer before freeing the UV request. (cherry picked from commit 6afc4270e0323a52262802d553dda8233df36d42) --- lib/isc/netmgr/http.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/isc/netmgr/http.c b/lib/isc/netmgr/http.c index 93ab907406..82742df510 100644 --- a/lib/isc/netmgr/http.c +++ b/lib/isc/netmgr/http.c @@ -2753,6 +2753,8 @@ server_httpsend(isc_nmhandle_t *handle, isc_nmsocket_t *sock, } else { cb(handle, result, cbarg); } + + isc_buffer_initnull(&sock->h2->wbuf); isc__nm_uvreq_put(&req); } From 005e151c5bf90440c02deb86875e958babeb25d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 6 May 2026 10:12:35 +0200 Subject: [PATCH 26/30] Pass empty string instead of NULL to ns_client_dumpmessage() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two new call sites added by the CLASS-validation work passed NULL as the reason, but ns_client_dumpmessage() bails out early on a NULL reason — so the message dump never happened. The intent was to dump the message and let the follow-up ns_client_log() carry the reason text, so pass "" to suppress the prefix without short-circuiting the dump. (cherry picked from commit 3401cbd16f44b4ecb8b57dc9d1951037db6d0e32) --- lib/ns/client.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ns/client.c b/lib/ns/client.c index c10b122629..4ee94289d5 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -2097,7 +2097,7 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, default: dns_rdataclass_format(client->message->rdclass, classbuf, sizeof(classbuf)); - ns_client_dumpmessage(client, NULL); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "invalid message class: %s", classbuf); @@ -2195,7 +2195,7 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); - ns_client_dumpmessage(client, NULL); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", From 79f71827d0bd5e3d9216547b66dfa499436046c1 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Fri, 8 May 2026 10:24:23 +0200 Subject: [PATCH 27/30] Generate changelog for BIND 9.20.23 --- doc/arm/changelog.rst | 1 + doc/changelog/changelog-9.20.23.rst | 369 ++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 doc/changelog/changelog-9.20.23.rst diff --git a/doc/arm/changelog.rst b/doc/arm/changelog.rst index 19f6bea113..2dea4fb0c8 100644 --- a/doc/arm/changelog.rst +++ b/doc/arm/changelog.rst @@ -18,6 +18,7 @@ Changelog development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.20.23.rst .. include:: ../changelog/changelog-9.20.22.rst .. include:: ../changelog/changelog-9.20.21.rst .. include:: ../changelog/changelog-9.20.20.rst diff --git a/doc/changelog/changelog-9.20.23.rst b/doc/changelog/changelog-9.20.23.rst new file mode 100644 index 0000000000..dcc6f0dd94 --- /dev/null +++ b/doc/changelog/changelog-9.20.23.rst @@ -0,0 +1,369 @@ +.. 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. + +BIND 9.20.23 +------------ + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. ``1006b044b7`` + + Unauthorized clients could consume outgoing zone transfers quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + +- [CVE-2026-3592] Limit resolver server list size. ``c3f3879560`` + + When resolving a domain with many nameservers that share overlapping + IP addresses (e.g., 10 NS records all pointing at the same set of + addresses), BIND could previously waste time querying duplicate + addresses and build up excessively large server lists. Deduplicate + addresses in the resolver's server list so that each unique IP is only + queried once per resolution attempt, regardless of how many NS records + point to it and cap the number of addresses stored per nameserver name + to 6 (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. :gl:`#5641` + +- [CVE-2026-3039] Fix GSS-API resource leak. ``92d5c60855`` + + Fixed a memory leak where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with tkey-gssapi-keytab configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. + + Also implemented missing RFC 3645 requirement: the client now verifies + that mutual authentication and integrity flags are granted by the + GSS-API mechanism (Section 3.1.1). :gl:`#5752` + +- [CVE-2026-5950] Avoid unbounded recursion loop. ``568be408bc`` + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- [CVE-2026-5947] Fix crash in resolver when SIG(0)-signed responses are + received under load. ``9831f41894`` + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. :gl:`#5819` + +- [CVE-2026-3593] Add system test for HTTP/2 SETTINGS frame flood. + ``3be272e26d`` + + A use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause named to crash when a client sends a flood of HTTP/2 + SETTINGS frames while a DoH response is being written. This affects + servers with DoH (DNS-over-HTTPS) enabled. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. + + For: #5755 + +- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. ``014be8be87`` + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with recursion yes in a non-IN view will log a warning at + startup, and named-checkconf flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. + +Removed Features +~~~~~~~~~~~~~~~~ + +- Remove obsolete KEY record EXTENDED flag deprecated by RFC 3445. + ``99c226576a`` + + KEY resource records originally defined EXTENDED flag that was removed + by RFC 3445 back in 2002. BIND still carried code to parse and emit + it, including the additional two-octet flags field that followed when + the EXTENDED bit was set. That handling has been removed and the + affected bit positions are now reserved. + + Dropping the extended-flags handling also eliminates a possible crash + that could be reached when signing a zone containing an invalid key. + :gl:`#5900` + + Partial backport of MR !11961 :gl:`!11962` + +Feature Changes +~~~~~~~~~~~~~~~ + +- Revert isdelegation() to return boolean value again. ``6d89bfdf03`` + + :gl:`#5838` :gl:`!11802` + +- Fix CPU spikes and slow queries when cache approaches memory limit. + ``e21ae6358a`` + + When the cache grew close to the configured max-cache-size, every + subsequent entry triggered all worker threads to run cache cleanup at + once, causing CPU spikes and a drop in query throughput. Cleanup is + now spread probabilistically across inserts as memory approaches the + limit, so the work is distributed evenly instead of piling up at the + threshold. + +- Fix off by one error in dnssec-ksr sign. ``819df0d19e`` + + If the inception time of the signature is exactly equal to the + inactive time of the key, add the signature. :gl:`!11795` + +Bug Fixes +~~~~~~~~~ + +- Check validator name when adding EDE text. ``b6c3390aea`` + + When a validator is being shut down, the associated name `val->name` + is set to NULL. This could cause a crash if a worker thread + subsequently added an EDE code with `val->name` in the extra text. + + `validator_addede()` now checks whether the name is NULL before trying + to add it to the extra text. :gl:`#5613` :gl:`!11977` + +- Use the zone file's basename as origin in DNSSEC tools. ``097c14da45`` + + In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not + specified using the `-o` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + `dnssec-signzone -S example.com` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, `dnssec-signzone -S zones/example.com` will set the origin + value to `example.com`. :gl:`#5678` :gl:`!11784` + +- Fix a possible race condition during zone transfers. ``a48b287d9f`` + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` :gl:`!11799` + +- Make BIND9 compatible with OpenSSL 4. ``8242105d5d`` + + OPENSSL_cleanup() in OpenSSL 4 doesn't free the memory, and that is + not compatible with BIND 9's memory leak detection code. Don't use + custom allocation/deallocation functions for OpenSSL's internal memory + management. + + See https://github.com/openssl/openssl/pull/29721 :gl:`#5808` + :gl:`!11896` + +- Fix named crash when processing SIG records in dynamic updates. + ``9e34ef0f7e`` + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` :gl:`!11876` + +- Fix crash in resolver when SIG(0)-signed responses are received under + load. ``bbe0b9b8f6`` + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. :gl:`#5819` + +- Fix zone verification of NSEC3 signed zones. ``de4a9b4fa6`` + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted mirror zones which are NSEC3-signed, + `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + :gl:`#5834` :gl:`!11833` + +- Prevent a crash when using both dns64 and filter-aaaa. ``ddcacbc5a8`` + + An assertion failure could be triggered if both `dns64` and the + `filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + :gl:`!11967` + +- Remove unnecessary dns_name_free call. ``35d94fffb0`` + + When processing a catalog zone member's primaries definition and there + is a TXT record containing an invalid name TSIG key name, + dns_name_free was incorrectly called triggering an assertion. This has + been fixed. :gl:`#5858` :gl:`!11848` + +- Tidy up the cleanup path in check_signer() ``cf517f73d5`` + + When check_signer() processed a DNSKEY whose public-key data could not + be parsed, the early return on the parse error skipped the cleanup of + the cloned signature rdataset. In every code path that currently + reaches this function the cloned rdataset holds no resources, so no + memory was actually leaked, but the cleanup is restructured so the + parse and the iteration cannot diverge again. :gl:`#5869` :gl:`!11957` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + ``c425827743`` + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` :gl:`!11923` + +- Fix inverted gethostname() check in rndc status. ``5ede4a87eb`` + + The replacement of named_os_gethostname() with raw gethostname() + inverted the success check: the "localhost" fallback runs on success, + and on failure the uninitialized hostname buffer is read by + snprintf(), leaking stack memory via the rndc status reply. + :gl:`#5889` :gl:`!11881` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + ``7e1eace6cd`` + + `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` :gl:`!11910` + +- Prevent crafted queries from degrading RRL performance. ``bf4cdca7e9`` + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` :gl:`!11952` + +- Fix swapped arguments in redirect2() single-label branch. + ``3728b405ea`` + + On a recursive resolver with nxdomain-redirect configured, an NXDOMAIN + result for a query whose qname is the root could corrupt the view's + nxdomain-redirect target, after which the redirect feature stopped + working for every subsequent query in that view until named was + restarted. :gl:`#5908` :gl:`!11913` + +- Free per-command rndc state when response serialisation fails. + ``070b394f53`` + + When isccc_cc_towire failed while building an rndc reply, + control_respond returned without releasing the per-command request, + response, HMAC secret copy, and text buffer. They were eventually + freed when the connection closed, but until then the HMAC key copy + stayed in named's memory. The failure path now goes through the same + cleanup label as every other error. :gl:`#5913` :gl:`!11919` + +- Prevent rare named crash when notifies are cancelled. ``49509dcbae`` + + Under heavy load, named could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + :gl:`!11922` + +- Stop delv from aborting on a malformed query name. ``ca8315bb4d`` + + delv aborts with SIGABRT instead of exiting cleanly when given a query + name that fails wire-format conversion (e.g. a label longer than 63 + octets). After this change delv prints the parse error and exits with + a normal failure code. :gl:`#5916` :gl:`!11927` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + ``971ca4df1a`` + + When named was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause `named` to + crash. This has been fixed. :gl:`#5938` :gl:`!11966` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. ``e962fd459e`` + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid ``allow-query`` or + ``allow-transfer`` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + :gl:`!11975` + +- Fix a stack use-after-free in qpzone. ``ddea991c07`` + + In previous_closest_nsec(), a new qpreader was opened to search the + NSEC tree. It was possible for that to be used to update a QP iterator + object owned by the caller, and then be destroyed when the function + returned. + + This has been addressed by having the caller open the NSEC qpreader + instead. :gl:`#5942` :gl:`!11956` + +- Fix a memory leak issue in the catalog zones. ``5fcb6d8809`` + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys using both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) at the + same time. This has been fixed. :gl:`#5943` :gl:`!11973` + +- Fix suppressed missing-glue check in named-checkzone. ``dc5eb3fe25`` + + named-checkzone and named-checkconf -z silently skipped the + missing-glue check for any NS name that had already triggered an + extra-AAAA-glue warning, so zones missing required A glue could pass + validation and be deployed with broken delegations. :gl:`!11905` + +- Implement seamless outgoing TCP connection reuse. ``eb117e16b9`` + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by RFC 7766. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into TIME_WAIT state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. :gl:`!11846` + +- Pass empty string instead of NULL to ns_client_dumpmessage() + ``24cdf8c096`` + + Pass "" instead of NULL to ns_client_dumpmessage() to get the log + message printed. + +- Reject record sets too large to serve in DNS. ``933a8de056`` + + When BIND was asked to store a record set whose total size exceeds + what fits in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. :gl:`!11964` + + From 6a65bf467b1aacdc66ad775797f7ece9ba652d19 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Fri, 8 May 2026 10:24:43 +0200 Subject: [PATCH 28/30] Prepare release notes for BIND 9.20.23 --- doc/arm/notes.rst | 1 + doc/notes/notes-9.20.23.rst | 246 ++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 doc/notes/notes-9.20.23.rst diff --git a/doc/arm/notes.rst b/doc/arm/notes.rst index d5b88c559c..a91b6c7d90 100644 --- a/doc/arm/notes.rst +++ b/doc/arm/notes.rst @@ -45,6 +45,7 @@ The list of known issues affecting the latest version in the 9.20 branch can be found at https://gitlab.isc.org/isc-projects/bind9/-/wikis/Known-Issues-in-BIND-9.20 +.. include:: ../notes/notes-9.20.23.rst .. include:: ../notes/notes-9.20.22.rst .. include:: ../notes/notes-9.20.21.rst .. include:: ../notes/notes-9.20.20.rst diff --git a/doc/notes/notes-9.20.23.rst b/doc/notes/notes-9.20.23.rst new file mode 100644 index 0000000000..1cbe34c8ac --- /dev/null +++ b/doc/notes/notes-9.20.23.rst @@ -0,0 +1,246 @@ +.. 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. + +Notes for BIND 9.20.23 +---------------------- + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. + + Unauthorized clients could consume outgoing zone transfers quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + +- [CVE-2026-3592] Limit resolver server list size. + + When resolving a domain with many nameservers that share overlapping + IP addresses (e.g., 10 NS records all pointing at the same set of + addresses), BIND could previously waste time querying duplicate + addresses and build up excessively large server lists. Deduplicate + addresses in the resolver's server list so that each unique IP is only + queried once per resolution attempt, regardless of how many NS records + point to it and cap the number of addresses stored per nameserver name + to 6 (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. :gl:`#5641` + +- [CVE-2026-3039] Fix GSS-API resource leak. + + Fixed a memory leak where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with tkey-gssapi-keytab configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. + + Also implemented missing RFC 3645 requirement: the client now verifies + that mutual authentication and integrity flags are granted by the + GSS-API mechanism (Section 3.1.1). :gl:`#5752` + +- [CVE-2026-5950] Avoid unbounded recursion loop. + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- [CVE-2026-3593] Add system test for HTTP/2 SETTINGS frame flood. + + A use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause named to crash when a client sends a flood of HTTP/2 + SETTINGS frames while a DoH response is being written. This affects + servers with DoH (DNS-over-HTTPS) enabled. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. + + For: #5755 + +- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with recursion yes in a non-IN view will log a warning at + startup, and named-checkconf flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. + +Feature Changes +~~~~~~~~~~~~~~~ + +- Fix CPU spikes and slow queries when cache approaches memory limit. + + When the cache grew close to the configured max-cache-size, every + subsequent entry triggered all worker threads to run cache cleanup at + once, causing CPU spikes and a drop in query throughput. Cleanup is + now spread probabilistically across inserts as memory approaches the + limit, so the work is distributed evenly instead of piling up at the + threshold. + +Bug Fixes +~~~~~~~~~ + +- Use the zone file's basename as origin in DNSSEC tools. + + In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not + specified using the `-o` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + `dnssec-signzone -S example.com` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, `dnssec-signzone -S zones/example.com` will set the origin + value to `example.com`. :gl:`#5678` + +- Fix a possible race condition during zone transfers. + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` + +- Fix named crash when processing SIG records in dynamic updates. + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` + +- Fix zone verification of NSEC3 signed zones. + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted mirror zones which are NSEC3-signed, + `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + :gl:`#5834` + +- Prevent a crash when using both dns64 and filter-aaaa. + + An assertion failure could be triggered if both `dns64` and the + `filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + +- Remove unnecessary dns_name_free call. + + When processing a catalog zone member's primaries definition and there + is a TXT record containing an invalid name TSIG key name, + dns_name_free was incorrectly called triggering an assertion. This has + been fixed. :gl:`#5858` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + + `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` + +- Prevent crafted queries from degrading RRL performance. + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` + +- Prevent rare named crash when notifies are cancelled. + + Under heavy load, named could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + +- Stop delv from aborting on a malformed query name. + + delv aborts with SIGABRT instead of exiting cleanly when given a query + name that fails wire-format conversion (e.g. a label longer than 63 + octets). After this change delv prints the parse error and exits with + a normal failure code. :gl:`#5916` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + + When named was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause `named` to + crash. This has been fixed. :gl:`#5938` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid ``allow-query`` or + ``allow-transfer`` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + +- Fix a memory leak issue in the catalog zones. + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys using both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) at the + same time. This has been fixed. :gl:`#5943` + +- Fix suppressed missing-glue check in named-checkzone. + + named-checkzone and named-checkconf -z silently skipped the + missing-glue check for any NS name that had already triggered an + extra-AAAA-glue warning, so zones missing required A glue could pass + validation and be deployed with broken delegations. + +- Implement seamless outgoing TCP connection reuse. + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by RFC 7766. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into TIME_WAIT state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. + +- Reject record sets too large to serve in DNS. + + When BIND was asked to store a record set whose total size exceeds + what fits in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. + + From 231f99328a533bb9de93d5aa02fd26e714b3edd2 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Fri, 8 May 2026 10:43:51 +0200 Subject: [PATCH 29/30] Tweak and reword release notes --- doc/notes/notes-9.20.23.rst | 200 ++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 91 deletions(-) diff --git a/doc/notes/notes-9.20.23.rst b/doc/notes/notes-9.20.23.rst index 1cbe34c8ac..a7e75f7641 100644 --- a/doc/notes/notes-9.20.23.rst +++ b/doc/notes/notes-9.20.23.rst @@ -15,41 +15,54 @@ Notes for BIND 9.20.23 Security Fixes ~~~~~~~~~~~~~~ -- Fix outgoing zone transfers' quota issue. +- Limit resolver server list size. :cve:`2026-3592` - Unauthorized clients could consume outgoing zone transfers quota and - block authorized zone transfer clients. This has been fixed. - :gl:`#3589` - -- [CVE-2026-3592] Limit resolver server list size. - - When resolving a domain with many nameservers that share overlapping + When resolving a domain with many nameservers that shared overlapping IP addresses (e.g., 10 NS records all pointing at the same set of addresses), BIND could previously waste time querying duplicate - addresses and build up excessively large server lists. Deduplicate - addresses in the resolver's server list so that each unique IP is only + addresses and build up excessively large server lists. Addresses in + the resolver's server list are now deduplicated so that each unique IP is only queried once per resolution attempt, regardless of how many NS records - point to it and cap the number of addresses stored per nameserver name - to 6 (combined A and AAAA), preventing memory and CPU overhead from - domains with unusually large NS/glue sets. :gl:`#5641` + point to it. The number of addresses stored per nameserver name + is also now capped at six (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. -- [CVE-2026-3039] Fix GSS-API resource leak. + ISC would like to thank Shuhan Zhang from Tsinghua University for + reporting this issue. :gl:`#5641` - Fixed a memory leak where each GSS-API TKEY negotiation leaked a +- Fix GSS-API resource leak. :cve:`2026-3039` + + A memory leak was fixed where each GSS-API TKEY negotiation leaked a security context inside the GSS library. An unauthenticated attacker could exhaust server memory by sending repeated TKEY queries to a - server with tkey-gssapi-keytab configured. The leaked memory was + server with :any:`tkey-gssapi-keytab` configured. The leaked memory was allocated by the GSS library, bypassing BIND's memory accounting. Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now rejected, as BIND never supported it correctly and Kerberos/SPNEGO completes in a single round. - Also implemented missing RFC 3645 requirement: the client now verifies - that mutual authentication and integrity flags are granted by the - GSS-API mechanism (Section 3.1.1). :gl:`#5752` + ISC would like to thank Vitaly Simonovich for bringing this + vulnerability to our attention. :gl:`#5752` -- [CVE-2026-5950] Avoid unbounded recursion loop. +- Disable recursion, UPDATE, and NOTIFY for non-IN views. + :cve:`2026-5946` + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with :namedconf:ref:`recursion yes; ` + in a non-IN view log a warning at + startup, and :iscman:`named-checkconf` flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. :gl:`#5784` + +- Avoid unbounded recursion loop. :cve:`2026-5950` A bug during bad server handling could cause the resolver to enter an infinite loop, continuously sending queries to an upstream server with @@ -59,60 +72,56 @@ Security Fixes ISC would like to thank Billy Baraja (BielraX) for bringing this issue to our attention. :gl:`#5804` -- [CVE-2026-3593] Add system test for HTTP/2 SETTINGS frame flood. +- Fix crash in resolver when SIG(0)-signed responses are received under + load. :cve:`2026-5947` - A use-after-free vulnerability in the DNS-over-HTTPS implementation - could cause named to crash when a client sends a flood of HTTP/2 - SETTINGS frames while a DoH response is being written. This affects - servers with DoH (DNS-over-HTTPS) enabled. + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. + + ISC would like to thank Naoki Wakamatsu for bringing this + vulnerability to our attention. :gl:`#5819` + +- Fix use-after-free error in DNS-over-HTTPS when processing HTTP/2 + SETTINGS frames. :cve:`2026-3593` + + Previously, a use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause :iscman:`named` to crash when a client sent a flood of HTTP/2 + SETTINGS frames while a DoH response was being written. This affected + servers with DoH (DNS-over-HTTPS) enabled and has been fixed. ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting - this. + this. :gl:`#5755` - For: #5755 +- Fix outgoing zone transfers' quota issue. -- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN - views. - - Recursion, dynamic updates (UPDATE), and zone change notifications - (NOTIFY) are now disabled for views with a class other than IN (such - as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. - version.bind in class CHAOS) continues to work as before. Servers - configured with recursion yes in a non-IN view will log a warning at - startup, and named-checkconf flags the same condition. UPDATE and - NOTIFY messages that specify the meta-classes ANY or NONE in the - question section are now rejected with FORMERR. - - This addresses a set of closely related security issues collectively - identified as CVE-2026-5946. ISC would like to thank Mcsky23 for - bringing these issues to our attention. + Unauthorized clients could consume the entire outgoing zone-transfer quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` Feature Changes ~~~~~~~~~~~~~~~ - Fix CPU spikes and slow queries when cache approaches memory limit. - When the cache grew close to the configured max-cache-size, every - subsequent entry triggered all worker threads to run cache cleanup at - once, causing CPU spikes and a drop in query throughput. Cleanup is - now spread probabilistically across inserts as memory approaches the - limit, so the work is distributed evenly instead of piling up at the - threshold. + Cache cleanup is now spread probabilistically to avoid CPU usage spikes and a + drop in query throughput. :gl:`#5891` Bug Fixes ~~~~~~~~~ - Use the zone file's basename as origin in DNSSEC tools. - In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not - specified using the `-o` parameter, the default behavior is to try to + In :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`, when the zone origin is not + specified using the ``-o`` parameter, the default behavior is to try to sign using the zone's file name as the origin. So, for example, - `dnssec-signzone -S example.com` will work, so long as the file name + ``dnssec-signzone -S example.com`` will work, so long as the file name matches the zone name. This now also works if the zone is in a different directory. For - example, `dnssec-signzone -S zones/example.com` will set the origin - value to `example.com`. :gl:`#5678` + example, ``dnssec-signzone -S zones/example.com`` will set the origin + value to ``example.com``. :gl:`#5678` - Fix a possible race condition during zone transfers. @@ -120,40 +129,47 @@ Bug Fixes processing an IXFR message during a zone transfer. This has been fixed. :gl:`#5767` -- Fix named crash when processing SIG records in dynamic updates. +- Fix :iscman:`named` crash when processing SIG records in dynamic updates. Previously, :iscman:`named` could abort if a client sent a dynamic update containing a SIG record (the legacy signature type) to a zone configured with an update-policy. The function `dns_db_findrdataset` had an incorrect requirements prerequisite that prevented SIG records - being looked up, which was triggered as part of processing an UPDATE + from being looked up, which was triggered as part of processing an UPDATE request and could be triggered remotely by any client permitted to send updates. This has been fixed by ensuring that SIG records are handled consistently with RRSIG records during update processing. :gl:`#5818` +- Fix :option:`rndc modzone` behavior for a zone in named.conf. + + If a zone was present in the configuration file and not originally + added by :option:`rndc addzone`, :option:`rndc modzone` for that zone would succeed + once but subsequent :option:`rndc modzone` attempts would fail. This has been + fixed. :gl:`#5826` + - Fix zone verification of NSEC3 signed zones. Previously, when computing the compressed bitmap during verification of an NSEC3-signed zone, an undersized buffer was used that resulted in an out-of-bounds write if there were too many active windows in the - bitmap. This impacted mirror zones which are NSEC3-signed, - `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + bitmap. This impacted the mirror zones which are NSEC3-signed, + :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`. This has been fixed. :gl:`#5834` -- Prevent a crash when using both dns64 and filter-aaaa. +- Prevent a crash when using both :any:`dns64` and :any:`filter-aaaa`. - An assertion failure could be triggered if both `dns64` and the - `filter-aaaa` plugin were in use simultaneously. This happened if the + An assertion failure could be triggered if both :any:`dns64` and the + :any:`filter-aaaa` plugin were in use simultaneously. This happened if the plugin triggered a second recursion process, which then attempted to store DNS64 state information in a pointer that had already been set by the original recursion process. This has been fixed. :gl:`#5854` -- Remove unnecessary dns_name_free call. +- Fixed an assertion failure when processing catalog zones. - When processing a catalog zone member's primaries definition and there - is a TXT record containing an invalid name TSIG key name, - dns_name_free was incorrectly called triggering an assertion. This has + If a TXT record containing an invalid name TSIG key name was found + when processing a catalog zone member's primaries definition, + ``dns_name_free`` was incorrectly called, triggering an assertion. This has been fixed. :gl:`#5858` - Prevent malicious DNSSEC zones from exhausting validator CPU. @@ -164,9 +180,10 @@ Bug Fixes validator now rejects such DNSKEYs, matching the limit already applied to keys read from files or HSMs. :gl:`#5881` -- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. +- Fix :iscman:`rndc-confgen` aborting on HMAC-SHA-384/512 keys above 512 bits. - `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + :iscman:`rndc-confgen` (with either ``-A hmac-sha384`` or + ``-A hmac-sha512``) previously documented a ``-b`` range of 1..1024, but any value above 512 aborted on hardened builds instead of producing a key. The full advertised range now works. :gl:`#5903` @@ -180,67 +197,68 @@ Bug Fixes placement of entries cannot be predicted or influenced from the network. :gl:`#5906` -- Prevent rare named crash when notifies are cancelled. +- Prevent rare :iscman:`named` crash when notifies are cancelled. - Under heavy load, named could occasionally crash when a queued + Under heavy load, :iscman:`named` could occasionally crash when a queued outbound notify or zone refresh was cancelled at the moment it was being sent — for example, while a zone was being reloaded or removed. The race that caused the crash is now prevented. :gl:`#5915` -- Stop delv from aborting on a malformed query name. +- Stop :iscman:`delv` from aborting on a malformed query name. - delv aborts with SIGABRT instead of exiting cleanly when given a query - name that fails wire-format conversion (e.g. a label longer than 63 - octets). After this change delv prints the parse error and exits with + :iscman:`delv` previously aborted with SIGABRT instead of exiting cleanly when given a query + name that failed wire-format conversion (e.g. a label longer than 63 + octets). After this change :iscman:`delv` prints the parse error and exits with a normal failure code. :gl:`#5916` - Fix a crash when reconfiguring while an NTA is being rechecked. - When named was reconfigured or shut down while a negative trust anchor + Previously, if :iscman:`named` was reconfigured or shut down while a negative trust anchor was being rechecked against authoritative servers, the in-flight - recheck could outlive the view that owned it and cause `named` to + recheck could outlive the view that owned it and cause :iscman:`named` to crash. This has been fixed. :gl:`#5938` -- Fix a bug in allow-query/allow-transfer catalog zone custom +- Fix a bug in :any:`allow-query`/:any:`allow-transfer` catalog zone custom properties. The :iscman:`named` process could terminate unexpectedly when - processing a catalog zone with an invalid ``allow-query`` or - ``allow-transfer`` custom property (i.e. having a non-APL type) + processing a catalog zone with an invalid :any:`allow-query` or + :any:`allow-transfer` custom property (i.e. having a non-APL type) coexisting with the valid property. This has been fixed. :gl:`#5941` -- Fix a memory leak issue in the catalog zones. +- Fix a memory leak issue in catalog zones. The :iscman:`named` process could leak small amounts of memory when processing a catalog zone entry which had defined custom primary - servers with TSIG keys using both the regular ``primaries`` custom - property syntax and the legacy alternative syntax (``masters``) at the + servers with TSIG keys, if both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) were used at the same time. This has been fixed. :gl:`#5943` -- Fix suppressed missing-glue check in named-checkzone. +- Fix suppressed missing-glue check in :iscman:`named-checkzone`. - named-checkzone and named-checkconf -z silently skipped the - missing-glue check for any NS name that had already triggered an - extra-AAAA-glue warning, so zones missing required A glue could pass - validation and be deployed with broken delegations. + :iscman:`named-checkzone` and :option:`named-checkconf -z` silently + skipped the missing-glue check for any NS name that had already + triggered an extra-AAAA-glue warning, so zones missing required A glue + could pass validation and be deployed with broken delegations. + :gl:`!11899` - Implement seamless outgoing TCP connection reuse. The resolver can and will reuse outgoing TCP connections to the same - host, as recommended by RFC 7766. This prevents a whole class of + host, as recommended by :rfc:`7766`. This prevents a whole class of attacks that abuse the fact that establishing a TCP connection is expensive and it is fairly easy to deplete the outgoing TCP ports by - putting them into TIME_WAIT state. + putting them into ``TIME_WAIT`` state. The number of pipelined queries per connection is capped at 256 to - limit the impact of a connection drop. + limit the impact of a connection drop. :gl:`!11845` - Reject record sets too large to serve in DNS. - When BIND was asked to store a record set whose total size exceeds - what fits in a DNS message, it would allocate memory and build the + When BIND was asked to store a record set whose total size exceeded + what fit in a DNS message, it would allocate memory and build the structure, then fail later at response time. Such oversized record sets are now rejected at the time of storage with an error, avoiding - wasted work on data that can never be served. + wasted work on data that can never be served. :gl:`!11963` From 7d0b4d4d436e7b950225a533e5bc56d8f935cc1c Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Fri, 8 May 2026 16:39:05 +0200 Subject: [PATCH 30/30] Update BIND version for release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 285f70d102..b0a53727da 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ m4_define([bind_VERSION_MAJOR], 9)dnl m4_define([bind_VERSION_MINOR], 20)dnl m4_define([bind_VERSION_PATCH], 23)dnl -m4_define([bind_VERSION_EXTRA], -dev)dnl +m4_define([bind_VERSION_EXTRA], )dnl m4_define([bind_DESCRIPTION], [(Stable Release)])dnl m4_define([bind_SRCID], [m4_esyscmd_s([git rev-parse --short HEAD | cut -b1-7])])dnl m4_define([bind_PKG_VERSION], [[bind_VERSION_MAJOR.bind_VERSION_MINOR.bind_VERSION_PATCH]bind_VERSION_EXTRA])dnl