commit d604a2e6757871b98e4fab5eb10b22aa8b2f6c55
Author: Ondřej Surý <ondrej@isc.org>
Date:   Wed May 6 09:33:41 2026 +0200

    [v9_11] [CVE-2026-5946] sec: usr: 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.
    
    Backport of https://gitlab.isc.org/isc-private/bind9/-/merge_requests/936
    
    Merge branch 'each-security-disable-chaos-recursion-security-bind-9.11' into 'security-bind-9.11'
    
    See merge request isc-private/bind9!1021

diff --git a/bin/named/client.c b/bin/named/client.c
index 863a8abfc1..82feed1d33 100644
--- a/bin/named/client.c
+++ b/bin/named/client.c
@@ -40,6 +40,7 @@
 #include <dns/dnstap.h>
 #include <dns/cache.h>
 #include <dns/edns.h>
+#include <dns/enumclass.h>
 #include <dns/events.h>
 #include <dns/message.h>
 #include <dns/peer.h>
@@ -2800,7 +2801,9 @@ client_request(isc_task_t *task, isc_event_t *event) {
 			goto cleanup;
 	}
 
-	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)
@@ -2822,6 +2825,44 @@ client_request(isc_task_t *task, isc_event_t *event) {
 				      "message class could not be determined");
 		ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR);
 		goto cleanup;
+	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, "");
+		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, DNS_R_NOTIMP);
+		goto cleanup;
 	}
 
 	/*
@@ -2940,7 +2981,7 @@ client_request(isc_task_t *task, isc_event_t *event) {
 		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");
+		ns_client_dumpmessage(client, "");
 		ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_REFUSED);
 		goto cleanup;
 	}
@@ -3119,11 +3160,19 @@ client_request(isc_task_t *task, isc_event_t *event) {
 		break;
 	case dns_opcode_update:
 		CTRACE("update");
+		if (client->view->rdclass != dns_rdataclass_in) {
+			ns_client_error(client, DNS_R_NOTIMP);
+			break;
+		}
 		ns_client_settimeout(client, 60);
 		ns_update_start(client, sigresult);
 		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);
 		break;
@@ -4193,8 +4242,9 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) {
 	int len = 1024;
 	isc_result_t result;
 
-	if (!isc_log_wouldlog(ns_g_lctx, ISC_LOG_DEBUG(1)))
+	if (!isc_log_wouldlog(ns_g_lctx, ISC_LOG_DEBUG(1)) || reason == NULL) {
 		return;
+	}
 
 	/*
 	 * Note that these are multiline debug messages.  We want a newline
diff --git a/bin/named/server.c b/bin/named/server.c
index 35440ff430..683ded78c1 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -1750,6 +1750,7 @@ 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(ns_g_server->zonemgr, zone);
 	if (result != ISC_R_SUCCESS)
 		return (result);
@@ -4107,32 +4108,14 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist,
 		CHECK(configure_alternates(config, view, alternates));
 
 	/*
-	 * 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, ns_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, &rootzone);
-		if (rootzone != NULL) {
-			dns_zone_detach(&rootzone);
-			need_hints = false;
-		}
-		if (need_hints)
-			isc_log_write(ns_g_lctx, NS_LOGCATEGORY_GENERAL,
-				      NS_LOGMODULE_SERVER, ISC_LOG_WARNING,
-				      "no root hints for view '%s'",
-				      view->name);
-	}
-
 	/*
 	 * Configure the view's TSIG keys.
 	 */
@@ -4239,7 +4222,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist,
 	obj = NULL;
 	result = ns_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));
 
 	obj = NULL;
 	result = ns_config_get(maps, "auth-nxdomain", &obj);
@@ -4346,10 +4330,10 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist,
 				 "allow-query-cache", NULL, actx,
 				 ns_g_mctx, &view->cacheacl));
 
-	if (strcmp(view->name, "_bind") != 0 &&
-	    view->rdclass != dns_rdataclass_chaos)
-	{
-		/* named.conf only */
+	if (view->rdclass != dns_rdataclass_in) {
+		dns_acl_none(ns_g_mctx, &view->recursionacl);
+		dns_acl_none(ns_g_mctx, &view->recursiononacl);
+	} else {
 		CHECK(configure_view_acl(vconfig, config, NULL,
 					 "allow-recursion", NULL, actx,
 					 ns_g_mctx, &view->recursionacl));
diff --git a/bin/named/update.c b/bin/named/update.c
index e9f6fa687c..411b92b28c 100644
--- a/bin/named/update.c
+++ b/bin/named/update.c
@@ -1265,7 +1265,10 @@ replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) {
 		    dbsig.algorithm == updatesig.algorithm)
 			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
@@ -1408,8 +1411,7 @@ add_rr_prepare_action(void *data, rr_t *rr) {
  * '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)
 {
@@ -1426,7 +1428,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;
 }
 
 /*%
@@ -1526,6 +1528,9 @@ send_update_event(ns_client_t *client, dns_zone_t *zone) {
 	isc_task_t *zonetask = NULL;
 	ns_client_t *evclient;
 
+	/* Updates are only supported for class IN. */
+	INSIST(dns_zone_getclass(zone) == dns_rdataclass_in);
+
 	event = (update_event_t *)
 		isc_event_allocate(client->mctx, client, DNS_EVENT_UPDATE,
 				   update_action, NULL, sizeof(*event));
@@ -2504,8 +2509,8 @@ update_action(isc_task_t *task, isc_event_t *event) {
 	isc_mem_t *mctx = client->mctx;
 	dns_rdatatype_t covers;
 	dns_message_t *request = client->message;
-	dns_rdataclass_t zoneclass;
 	dns_name_t *zonename;
+	dns_rdataclass_t zoneclass;
 	dns_ssutable_t *ssutable = NULL;
 	dns_fixedname_t tmpnamefixed;
 	dns_name_t *tmpname = NULL;
@@ -2536,6 +2541,7 @@ update_action(isc_task_t *task, isc_event_t *event) {
 	CHECK(checkqueryacl(client, dns_zone_getqueryacl(zone), zonename,
 			    dns_zone_getupdateacl(zone), ssutable));
 
+	INSIST(dns_zone_getclass(zone) == dns_rdataclass_in);
 	/*
 	 * Get old and new versions now that queryacl has been checked.
 	 */
@@ -2556,8 +2562,8 @@ update_action(isc_task_t *task, isc_event_t *event) {
 		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,
@@ -2615,7 +2621,7 @@ update_action(isc_task_t *task, isc_event_t *event) {
 						     "satisfied");
 				}
 			}
-		} else if (update_class == zoneclass) {
+		} else if (update_class == dns_rdataclass_in) {
 			/* "temp<rr.name, rr.type> += rr;" */
 			result = temp_append(&temp, name, &rdata);
 			if (result != ISC_R_SUCCESS) {
@@ -2686,7 +2692,7 @@ update_action(isc_task_t *task, isc_event_t *event) {
 		dns_rdata_t rdata = DNS_RDATA_INIT;
 		dns_ttl_t ttl;
 		dns_rdataclass_t update_class;
-		get_current_rr(request, DNS_SECTION_UPDATE, zoneclass,
+		get_current_rr(request, DNS_SECTION_UPDATE,
 			       &name, &rdata, &covers, &ttl, &update_class);
 
 		if (! dns_name_issubdomain(name, zonename))
@@ -2797,11 +2803,10 @@ update_action(isc_task_t *task, isc_event_t *event) {
 		dns_rdataclass_t update_class;
 		bool flag;
 
-		get_current_rr(request, DNS_SECTION_UPDATE, zoneclass,
-			       &name, &rdata, &covers, &ttl, &update_class);
-
-		if (update_class == zoneclass) {
+		get_current_rr(request, DNS_SECTION_UPDATE, &name,
+			       &rdata, &covers, &ttl, &update_class);
 
+		if (update_class == dns_rdataclass_in) {
 			/*
 			 * RFC1123 doesn't allow MF and MD in master zones.
 			 */
diff --git a/bin/tests/system/unknown/tests.sh b/bin/tests/system/unknown/tests.sh
index 190b84020d..344e79f9ee 100644
--- a/bin/tests/system/unknown/tests.sh
+++ b/bin/tests/system/unknown/tests.sh
@@ -73,8 +73,8 @@ echo_i "querying for various representations of a CLASS10 TYPE1 record"
 for i in 1 2
 do
 	ret=0
-	$DIG +short $DIGOPTS @10.53.0.1 a$i.example a class10 > dig.out || ret=1
-	echo '\# 4 0A000001' | $DIFF - dig.out || ret=1
+	$DIG $DIGOPTS @10.53.0.1 a$i.example a class10 > dig.out || ret=1
+        grep -q "NOTIMP" dig.out || ret=1
 	if [ $ret != 0 ]
 	then
 		echo "#$i failed"
@@ -86,8 +86,8 @@ echo_i "querying for various representations of a CLASS10 TXT record"
 for i in 1 2 3 4
 do
 	ret=0
-	$DIG +short $DIGOPTS @10.53.0.1 txt$i.example txt class10 > dig.out || ret=1
-	echo '"hello"' | $DIFF - dig.out || ret=1
+	$DIG $DIGOPTS @10.53.0.1 txt$i.example txt class10 > dig.out || ret=1
+        grep -q "NOTIMP" dig.out || ret=1
 	if [ $ret != 0 ]
 	then
 		echo "#$i failed"
@@ -99,8 +99,8 @@ echo_i "querying for various representations of a CLASS10 TYPE123 record"
 for i in 1 2
 do
 	ret=0
-	$DIG +short $DIGOPTS @10.53.0.1 unk$i.example type123 class10 > dig.out || ret=1
-	echo '\# 1 00' | $DIFF - dig.out || ret=1
+	$DIG $DIGOPTS @10.53.0.1 unk$i.example type123 class10 > dig.out || ret=1
+        grep -q "NOTIMP" dig.out || ret=1
 	if [ $ret != 0 ]
 	then
 		echo "#$i failed"
diff --git a/lib/dns/adb.c b/lib/dns/adb.c
index 5a99dc9f30..a46439db9d 100644
--- a/lib/dns/adb.c
+++ b/lib/dns/adb.c
@@ -898,7 +898,7 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset,
 	INSIST(DNS_ADB_VALID(adb));
 
 	rdtype = rdataset->type;
-	INSIST((rdtype == dns_rdatatype_a) || (rdtype == dns_rdatatype_aaaa));
+	REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa);
 	if (rdtype == dns_rdatatype_a)
 		findoptions = DNS_ADBFIND_INET;
 	else
diff --git a/lib/dns/message.c b/lib/dns/message.c
index 4e4427e1cb..e80275ee63 100644
--- a/lib/dns/message.c
+++ b/lib/dns/message.c
@@ -1157,6 +1157,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.
diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
index 39f0478f71..20a81ddcef 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -6423,9 +6423,16 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name,
 	}
 
 	/*
-	 * Otherwise, search the filter list for a match for each address
-	 * record.  If a match is found, the address should be filtered,
-	 * so should the entire answer.
+	 * 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
+	 * filtered, so should the entire answer.
 	 */
 	for (result = dns_rdataset_first(rdataset);
 	     result == ISC_R_SUCCESS;
