From 128b3b676eb9413b4d25fb29c560895cfbbfa92e Mon Sep 17 00:00:00 2001
From: Evan Hunt <each@isc.org>
Date: Thu, 1 Sep 2022 16:05:04 -0700
Subject: [PATCH] add an update quota

limit the number of simultaneous DNS UPDATE events that can be
processed by adding a quota for update and update forwarding.
this quota currently, arbitrarily, defaults to 100.

also add a statistics counter to record when the update quota
has been exceeded.

(cherry picked from commit 7c47254a140c3e9cf383cda73c7b6a55c4782826)
---
 bin/named/bind9.xsl              |  2 +-
 bin/named/bind9.xsl.h            |  8 +++++++-
 bin/named/include/named/server.h |  7 ++++++-
 bin/named/server.c               |  3 +++
 bin/named/statschannel.c         |  5 +++--
 bin/named/update.c               | 34 +++++++++++++++++++++++++++++++-
 doc/arm/Bv9ARM-book.xml          | 15 ++++++++++++++
 7 files changed, 68 insertions(+), 6 deletions(-)

diff --git a/bin/named/bind9.xsl b/bin/named/bind9.xsl
index 9a1c6ff..85fd4c4 100644
--- a/bin/named/bind9.xsl
+++ b/bin/named/bind9.xsl
@@ -12,7 +12,7 @@
 
 <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0">
   <xsl:output method="html" indent="yes" version="4.0"/>
-  <xsl:template match="statistics[@version=&quot;3.8&quot;]">
+  <xsl:template match="statistics[@version=&quot;3.8.1&quot;]">
     <html>
       <head>
         <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
diff --git a/bin/named/bind9.xsl.h b/bin/named/bind9.xsl.h
index 9ce8cd7..5e0a892 100644
--- a/bin/named/bind9.xsl.h
+++ b/bin/named/bind9.xsl.h
@@ -17,7 +17,13 @@ static char xslmsg[] =
 	"\n"
 	"<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xmlns=\"http://www.w3.org/1999/xhtml\" version=\"1.0\">\n"
 	" <xsl:output method=\"html\" indent=\"yes\" version=\"4.0\"/>\n"
-	" <xsl:template match=\"statistics[@version=&quot;3.8&quot;]\">\n"
+#if 0
+	" <!-- the version number **below** must match version in "
+	"bin/named/statschannel.c -->\n"
+	" <!-- don't forget to update \"/xml/v<STATS_XML_VERSION_MAJOR>\" in "
+	"the HTTP endpoints listed below -->\n"
+#endif
+	" <xsl:template match=\"statistics[@version=&quot;3.8.1&quot;]\">\n"
 	" <html>\n"
 	" <head>\n"
 	" <script type=\"text/javascript\" src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js\"></script>\n"
diff --git a/bin/named/include/named/server.h b/bin/named/include/named/server.h
index 08a02dc..259acc7 100644
--- a/bin/named/include/named/server.h
+++ b/bin/named/include/named/server.h
@@ -137,6 +137,9 @@ struct ns_server {
 
 	uint16_t		transfer_tcp_message_size;
 	isc_rng_t *		rngctx;
+
+/* CVE-2022-3094 */
+	isc_quota_t		updquota;
 };
 
 struct ns_altsecret {
@@ -230,7 +233,9 @@ enum {
 	dns_nsstatscounter_trystale = 59,
 	dns_nsstatscounter_usedstale = 60,
 
-	dns_nsstatscounter_max = 61
+	dns_nsstatscounter_updatequota = 61,
+
+	dns_nsstatscounter_max = 62
 };
 
 /*%
diff --git a/bin/named/server.c b/bin/named/server.c
index 2d2fa0e..f09b895 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -9143,6 +9143,8 @@ ns_server_create(isc_mem_t *mctx, ns_server_t **serverp) {
 	RUNTIME_CHECK(result == ISC_R_SUCCESS);
 	result = isc_quota_init(&server->recursionquota, 100);
 	RUNTIME_CHECK(result == ISC_R_SUCCESS);
+	result = isc_quota_init(&server->updquota, 100);
+	RUNTIME_CHECK(result == ISC_R_SUCCESS);
 
 	result = dns_aclenv_init(mctx, &server->aclenv);
 	RUNTIME_CHECK(result == ISC_R_SUCCESS);
@@ -9410,6 +9412,7 @@ ns_server_destroy(ns_server_t **serverp) {
 
 	dns_aclenv_destroy(&server->aclenv);
 
+	isc_quota_destroy(&server->updquota);
 	isc_quota_destroy(&server->recursionquota);
 	isc_quota_destroy(&server->tcpquota);
 	isc_quota_destroy(&server->xfroutquota);
diff --git a/bin/named/statschannel.c b/bin/named/statschannel.c
index 56a9c21..1e8723c 100644
--- a/bin/named/statschannel.c
+++ b/bin/named/statschannel.c
@@ -300,6 +300,7 @@ init_desc(void) {
 	SET_NSSTATDESC(reclimitdropped,
 		       "queries dropped due to recursive client limit",
 		       "RecLimitDropped");
+	SET_NSSTATDESC(updatequota, "Update quota exceeded", "UpdateQuota");
 	SET_NSSTATDESC(trystale,
 		       "attempts to use stale cache data after lookup failure",
 		       "QryTryStale");
@@ -1546,7 +1547,7 @@ generatexml(ns_server_t *server, uint32_t flags,
 			ISC_XMLCHAR "type=\"text/xsl\" href=\"/bind9.xsl\""));
 	TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "statistics"));
 	TRY0(xmlTextWriterWriteAttribute(writer, ISC_XMLCHAR "version",
-					 ISC_XMLCHAR "3.8"));
+					 ISC_XMLCHAR "3.8.1"));
 
 	/* Set common fields for statistics dump */
 	dumparg.type = isc_statsformat_xml;
@@ -2303,7 +2304,7 @@ generatejson(ns_server_t *server, size_t *msglen,
 	/*
 	 * These statistics are included no matter which URL we use.
 	 */
-	obj = json_object_new_string("1.2");
+	obj = json_object_new_string("1.2.1");
 	CHECKMEM(obj);
 	json_object_object_add(bindstats, "json-stats-version", obj);
 
diff --git a/bin/named/update.c b/bin/named/update.c
index 6ad7d27..dccc543 100644
--- a/bin/named/update.c
+++ b/bin/named/update.c
@@ -1526,6 +1526,17 @@ send_update_event(ns_client_t *client, dns_zone_t *zone) {
 	isc_task_t *zonetask = NULL;
 	ns_client_t *evclient;
 
+	result = isc_quota_attach(&ns_g_server->updquota,
+				  &(isc_quota_t *){ NULL });
+	if (result != ISC_R_SUCCESS) {
+		update_log(client, zone, LOGLEVEL_PROTOCOL,
+			   "update failed: too many DNS UPDATEs queued (%s)",
+			   isc_result_totext(result));
+		isc_stats_increment(ns_g_server->nsstats,
+				    dns_nsstatscounter_updatequota);
+		CHECK(DNS_R_DROP);
+	}
+
 	event = (update_event_t *)
 		isc_event_allocate(client->mctx, client, DNS_EVENT_UPDATE,
 				   update_action, NULL, sizeof(*event));
@@ -1652,7 +1663,12 @@ ns_update_start(ns_client_t *client, isc_result_t sigresult) {
 	 * We are still in the client task context, so we can
 	 * simply give an error response without switching tasks.
 	 */
-	respond(client, result);
+	if (result == DNS_R_DROP) {
+		ns_client_next(client, result);
+	} else {
+		respond(client, result);
+	}
+
 	if (zone != NULL)
 		dns_zone_detach(&zone);
 }
@@ -3385,6 +3401,7 @@ updatedone_action(isc_task_t *task, isc_event_t *event) {
 		dns_zone_detach(&uev->zone);
 	client->nupdates--;
 	respond(client, uev->result);
+	isc_quota_detach(&(isc_quota_t *){ &ns_g_server->updquota });
 	isc_event_free(&event);
 	ns_client_detach(&client);
 }
@@ -3402,6 +3419,8 @@ forward_fail(isc_task_t *task, isc_event_t *event) {
 	INSIST(client->nupdates > 0);
 	client->nupdates--;
 	respond(client, DNS_R_SERVFAIL);
+
+	isc_quota_detach(&(isc_quota_t *){ &ns_g_server->updquota });
 	isc_event_free(&event);
 	ns_client_detach(&client);
 }
@@ -3439,6 +3458,8 @@ forward_done(isc_task_t *task, isc_event_t *event) {
 	client->nupdates--;
 	ns_client_sendraw(client, uev->answer);
 	dns_message_detach(&uev->answer);
+
+	isc_quota_detach(&(isc_quota_t *){ &ns_g_server->updquota });
 	isc_event_free(&event);
 	ns_client_detach(&client);
 }
@@ -3472,6 +3493,17 @@ send_forward_event(ns_client_t *client, dns_zone_t *zone) {
 	isc_task_t *zonetask = NULL;
 	ns_client_t *evclient;
 
+	result = isc_quota_attach(&ns_g_server->updquota,
+				  &(isc_quota_t *){ NULL });
+	if (result != ISC_R_SUCCESS) {
+		update_log(client, zone, LOGLEVEL_PROTOCOL,
+			   "update failed: too many DNS UPDATEs queued (%s)",
+			   isc_result_totext(result));
+		isc_stats_increment(ns_g_server->nsstats,
+				    dns_nsstatscounter_updatequota);
+		return (DNS_R_DROP);
+	}
+
 	/*
 	 * This may take some time so replace this client.
 	 */
diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml
index c17f168..9aca6d7 100644
--- a/doc/arm/Bv9ARM-book.xml
+++ b/doc/arm/Bv9ARM-book.xml
@@ -15105,6 +15105,21 @@ HOST-127.EXAMPLE. MX 0 .
 		      </para>
 		    </entry>
 		  </row>
+		  <row rowsep="0">
+		    <entry colname="1">
+		      <para><command>UpdateQuota</command></para>
+		    </entry>
+		    <entry colname="2">
+		      <para><command/></para>
+		    </entry>
+		    <entry colname="3">
+		      <para>
+			This indicates the number of times a dynamic update or update
+			forwarding request was rejected because the number of pending
+			requests exceeded the update quota.
+		      </para>
+		    </entry>
+		  </row>
 		  <row rowsep="0">
 		    <entry colname="1">
 		      <para><command>RateDropped</command></para>
-- 
2.39.2