File bind-CVE-2016-6170.patch of Package bind

From 926433f157d9aee06e343515c1cedaa7758b0920 Mon Sep 17 00:00:00 2001
Message-Id: <926433f157d9aee06e343515c1cedaa7758b0920.1489674685.git.npajkovsky@suse.cz>
From: Nikola Pajkovsky <npajkovsky@suse.cz>
Date: Tue, 14 Mar 2017 13:26:57 +0100
Subject: [PATCH] 4504.   [security]      Allow the maximum number of records
 in a zone to                         be specified.  This provides a control
 for issues                         raised in CVE-2016-6170. [RT #42143]

Signed-off-by: Nikola Pajkovsky <npajkovsky@suse.cz>
---
 bin/named/config.c                               |   1 +
 bin/named/named.conf.docbook                     |   3 +
 bin/named/update.c                               |  16 +++
 bin/named/zoneconf.c                             |   7 ++
 bin/tests/system/nsupdate/clean.sh               |   1 +
 bin/tests/system/nsupdate/ns3/named.conf         |   7 ++
 bin/tests/system/nsupdate/ns3/too-big.test.db.in |  10 ++
 bin/tests/system/nsupdate/setup.sh               |   2 +
 bin/tests/system/nsupdate/tests.sh               |  15 +++
 bin/tests/system/xfer/clean.sh                   |   1 +
 bin/tests/system/xfer/ns1/axfr-too-big.db        |  10 ++
 bin/tests/system/xfer/ns1/ixfr-too-big.db.in     |  13 +++
 bin/tests/system/xfer/ns1/named.conf             |  11 +++
 bin/tests/system/xfer/ns6/named.conf             |  14 +++
 bin/tests/system/xfer/setup.sh                   |   2 +
 bin/tests/system/xfer/tests.sh                   |  26 +++++
 doc/arm/Bv9ARM-book.xml                          |  21 ++++
 doc/arm/notes.xml                                |   6 +-
 lib/bind9/check.c                                |   2 +
 lib/dns/db.c                                     |  13 +++
 lib/dns/ecdb.c                                   |   3 +-
 lib/dns/include/dns/db.h                         |  20 ++++
 lib/dns/include/dns/rdataslab.h                  |  13 +++
 lib/dns/include/dns/result.h                     |   3 +-
 lib/dns/include/dns/zone.h                       |  25 +++++
 lib/dns/rbtdb.c                                  | 121 ++++++++++++++++++++++-
 lib/dns/rdataslab.c                              |  13 +++
 lib/dns/result.c                                 |   1 +
 lib/dns/sdb.c                                    |   3 +-
 lib/dns/sdlz.c                                   |   3 +-
 lib/dns/xfrin.c                                  |  22 ++++-
 lib/dns/zone.c                                   |  23 ++++-
 lib/isccfg/namedconf.c                           |   1 +
 33 files changed, 420 insertions(+), 12 deletions(-)
 create mode 100644 bin/tests/system/nsupdate/ns3/too-big.test.db.in
 create mode 100644 bin/tests/system/xfer/ns1/axfr-too-big.db
 create mode 100644 bin/tests/system/xfer/ns1/ixfr-too-big.db.in

diff --git a/bin/named/config.c b/bin/named/config.c
index 4798272ac353..c5ee16169a29 100644
--- a/bin/named/config.c
+++ b/bin/named/config.c
@@ -198,6 +198,7 @@ options {\n\
 	max-transfer-time-out 120;\n\
 	max-transfer-idle-in 60;\n\
 	max-transfer-idle-out 60;\n\
+	max-records 0;\n\
 	max-retry-time 1209600; /* 2 weeks */\n\
 	min-retry-time 500;\n\
 	max-refresh-time 2419200; /* 4 weeks */\n\
diff --git a/bin/named/named.conf.docbook b/bin/named/named.conf.docbook
index 01cb62aaa009..2df51ded16f8 100644
--- a/bin/named/named.conf.docbook
+++ b/bin/named/named.conf.docbook
@@ -338,6 +338,7 @@ options {
 	};
 
 	max-journal-size <replaceable>size_no_default</replaceable>;
+	max-records <replaceable>integer</replaceable>;
 	max-transfer-time-in <replaceable>integer</replaceable>;
 	max-transfer-time-out <replaceable>integer</replaceable>;
 	max-transfer-idle-in <replaceable>integer</replaceable>;
@@ -527,6 +528,7 @@ view <replaceable>string</replaceable> <replaceable>optional_class</replaceable>
 	};
 
 	max-journal-size <replaceable>size_no_default</replaceable>;
+	max-records <replaceable>integer</replaceable>;
 	max-transfer-time-in <replaceable>integer</replaceable>;
 	max-transfer-time-out <replaceable>integer</replaceable>;
 	max-transfer-idle-in <replaceable>integer</replaceable>;
@@ -624,6 +626,7 @@ zone <replaceable>string</replaceable> <replaceable>optional_class</replaceable>
 	};
 
 	max-journal-size <replaceable>size_no_default</replaceable>;
+	max-records <replaceable>integer</replaceable>;
 	max-transfer-time-in <replaceable>integer</replaceable>;
 	max-transfer-time-out <replaceable>integer</replaceable>;
 	max-transfer-idle-in <replaceable>integer</replaceable>;
diff --git a/bin/named/update.c b/bin/named/update.c
index badf8fe1081f..ba4c37021cc0 100644
--- a/bin/named/update.c
+++ b/bin/named/update.c
@@ -2455,6 +2455,8 @@ update_action(isc_task_t *task, isc_event_t *event) {
 	dns_rdata_dnskey_t dnskey;
 	isc_boolean_t had_dnskey;
 	dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone);
+	isc_uint32_t maxrecords;
+	isc_uint64_t records;
 
 	INSIST(event->ev_type == DNS_EVENT_UPDATE);
 
@@ -3108,6 +3110,20 @@ update_action(isc_task_t *task, isc_event_t *event) {
 			}
 		}
 
+		maxrecords = dns_zone_getmaxrecords(zone);
+		if (maxrecords != 0U) {
+			result = dns_db_getsize(db, ver, &records, NULL);
+			if (result == ISC_R_SUCCESS && records > maxrecords) {
+				update_log(client, zone, ISC_LOG_ERROR,
+					   "records in zone (%"
+					   ISC_PRINT_QUADFORMAT
+					   "u) exceeds max-records (%u)",
+					   records, maxrecords);
+				result = DNS_R_TOOMANYRECORDS;
+				goto failure;
+			}
+		}
+
 		journalfile = dns_zone_getjournal(zone);
 		if (journalfile != NULL) {
 			update_log(client, zone, LOGLEVEL_DEBUG,
diff --git a/bin/named/zoneconf.c b/bin/named/zoneconf.c
index 5b473d1b2951..26fe0b7e66a4 100644
--- a/bin/named/zoneconf.c
+++ b/bin/named/zoneconf.c
@@ -935,6 +935,13 @@ ns_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig,
 			INSIST(0);
 	}
 
+	obj = NULL;
+	result = ns_config_get(maps, "max-records", &obj);
+	INSIST(result == ISC_R_SUCCESS && obj != NULL);
+	dns_zone_setmaxrecords(mayberaw, cfg_obj_asuint32(obj));
+	if (zone != mayberaw)
+		dns_zone_setmaxrecords(zone, 0);
+
 	if (raw != NULL && filename != NULL) {
 #define SIGNED ".signed"
 		size_t signedlen = strlen(filename) + sizeof(SIGNED);
diff --git a/bin/tests/system/nsupdate/clean.sh b/bin/tests/system/nsupdate/clean.sh
index dcb80d36eca6..57120a6f35ff 100644
--- a/bin/tests/system/nsupdate/clean.sh
+++ b/bin/tests/system/nsupdate/clean.sh
@@ -36,3 +36,4 @@ rm -f ns3/K*
 rm -f dig.out.*
 rm -f jp.out.ns3.*
 rm -f Kxxx.*
+rm -f ns3/too-big.test.db
diff --git a/bin/tests/system/nsupdate/ns3/named.conf b/bin/tests/system/nsupdate/ns3/named.conf
index 4b43efe4f22d..f38a7daf564b 100644
--- a/bin/tests/system/nsupdate/ns3/named.conf
+++ b/bin/tests/system/nsupdate/ns3/named.conf
@@ -60,3 +60,10 @@ zone "dnskey.test" {
 	allow-update { any; };
 	file "dnskey.test.db.signed";
 };
+
+zone "too-big.test" {
+	type master;
+	allow-update { any; };
+	max-records 3;
+	file "too-big.test.db";
+};
diff --git a/bin/tests/system/nsupdate/ns3/too-big.test.db.in b/bin/tests/system/nsupdate/ns3/too-big.test.db.in
new file mode 100644
index 000000000000..7ff1e4a514a4
--- /dev/null
+++ b/bin/tests/system/nsupdate/ns3/too-big.test.db.in
@@ -0,0 +1,10 @@
+; Copyright (C) 2016  Internet Systems Consortium, Inc. ("ISC")
+;
+; 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 http://mozilla.org/MPL/2.0/.
+
+$TTL 10
+too-big.test. IN SOA too-big.test. hostmaster.too-big.test. 1 3600 900 2419200 3600
+too-big.test. IN NS too-big.test.
+too-big.test. IN A 10.53.0.3
diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh
index 828255ee530b..43c40947de1b 100644
--- a/bin/tests/system/nsupdate/setup.sh
+++ b/bin/tests/system/nsupdate/setup.sh
@@ -27,12 +27,14 @@ test -r $RANDFILE || $GENRANDOM 400 $RANDFILE
 rm -f ns1/*.jnl ns1/example.db ns2/*.jnl ns2/example.bk
 rm -f ns2/update.bk ns2/update.alt.bk
 rm -f ns3/example.db.jnl
+rm -f ns3/too-big.test.db.jnl
 
 cp -f ns1/example1.db ns1/example.db
 sed 's/example.nil/other.nil/g' ns1/example1.db > ns1/other.db
 sed 's/example.nil/unixtime.nil/g' ns1/example1.db > ns1/unixtime.db
 sed 's/example.nil/keytests.nil/g' ns1/example1.db > ns1/keytests.db
 cp -f ns3/example.db.in ns3/example.db
+cp -f ns3/too-big.test.db.in ns3/too-big.test.db
 
 # update_test.pl has its own zone file because it
 # requires a specific NS record set.
diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh
index 799220d9c374..32840753db03 100644
--- a/bin/tests/system/nsupdate/tests.sh
+++ b/bin/tests/system/nsupdate/tests.sh
@@ -543,5 +543,20 @@ if [ $ret -ne 0 ]; then
     status=1
 fi
 
+n=`expr $n + 1`
+echo "I:check that adding too many records is blocked ($n)"
+ret=0
+$NSUPDATE -v << EOF > nsupdate.out-$n 2>&1 && ret=1
+server 10.53.0.3 5300
+zone too-big.test.
+update add r1.too-big.test 3600 IN TXT r1.too-big.test
+send
+EOF
+grep "update failed: SERVFAIL" nsupdate.out-$n > /dev/null || ret=1
+DIG +tcp @10.53.0.3 -p 5300 r1.too-big.test TXT > dig.out.ns3.test$n
+grep "status: NXDOMAIN" dig.out.ns3.test$n > /dev/null || ret=1
+grep "records in zone (4) exceeds max-records (3)" ns3/named.run > /dev/null || ret=1
+[ $ret = 0 ] || { echo I:failed; status=1; }
+
 echo "I:exit status: $status"
 exit $status
diff --git a/bin/tests/system/xfer/clean.sh b/bin/tests/system/xfer/clean.sh
index 58743ea9065e..4ee92d30bc5f 100644
--- a/bin/tests/system/xfer/clean.sh
+++ b/bin/tests/system/xfer/clean.sh
@@ -32,6 +32,7 @@ rm -f ns3/master.bk ns3/master.bk.jnl
 rm -f ns4/named.conf ns4/nil.db ns4/root.db
 rm -f ns6/*.db ns6/*.bk ns6/*.jnl
 rm -f ns7/*.db ns7/*.bk ns7/*.jnl
+rm -f ns1/ixfr-too-big.db ns1/ixfr-too-big.db.jnl
 
 rm -f */named.memstats
 rm -f */named.run
diff --git a/bin/tests/system/xfer/ns1/axfr-too-big.db b/bin/tests/system/xfer/ns1/axfr-too-big.db
new file mode 100644
index 000000000000..d43760d9a8d9
--- /dev/null
+++ b/bin/tests/system/xfer/ns1/axfr-too-big.db
@@ -0,0 +1,10 @@
+; Copyright (C) 2016  Internet Systems Consortium, Inc. ("ISC")
+;
+; 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 http://mozilla.org/MPL/2.0/.
+
+$TTL	3600
+@	IN	SOA	. . 0 0 0 0 0
+@	IN	NS	.
+$GENERATE 1-29	host$	A	1.2.3.$
diff --git a/bin/tests/system/xfer/ns1/ixfr-too-big.db.in b/bin/tests/system/xfer/ns1/ixfr-too-big.db.in
new file mode 100644
index 000000000000..318bb772af30
--- /dev/null
+++ b/bin/tests/system/xfer/ns1/ixfr-too-big.db.in
@@ -0,0 +1,13 @@
+; Copyright (C) 2016  Internet Systems Consortium, Inc. ("ISC")
+;
+; 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 http://mozilla.org/MPL/2.0/.
+
+$TTL	3600
+@	IN	SOA	. . 0 0 0 0 0
+@	IN	NS	ns1
+@	IN	NS	ns6
+ns1	IN	A	10.53.0.1
+ns6	IN	A	10.53.0.6
+$GENERATE 1-25	host$	A	1.2.3.$
diff --git a/bin/tests/system/xfer/ns1/named.conf b/bin/tests/system/xfer/ns1/named.conf
index 07dad85d9baa..1d292924c017 100644
--- a/bin/tests/system/xfer/ns1/named.conf
+++ b/bin/tests/system/xfer/ns1/named.conf
@@ -44,3 +44,14 @@ zone "slave" {
 	type master;
 	file "slave.db";
 };
+
+zone "axfr-too-big" {
+        type master;
+        file "axfr-too-big.db";
+};
+
+zone "ixfr-too-big" {
+        type master;
+	allow-update { any; };
+        file "ixfr-too-big.db";
+};
diff --git a/bin/tests/system/xfer/ns6/named.conf b/bin/tests/system/xfer/ns6/named.conf
index c9421b1f6558..a12a92c2f6d5 100644
--- a/bin/tests/system/xfer/ns6/named.conf
+++ b/bin/tests/system/xfer/ns6/named.conf
@@ -52,3 +52,17 @@ zone "slave" {
 	masters { 10.53.0.1; };
 	file "slave.bk";
 };
+
+zone "axfr-too-big" {
+	type slave;
+	max-records 30;
+	masters { 10.53.0.1; };
+	file "axfr-too-big.bk";
+};
+
+zone "ixfr-too-big" {
+	type slave;
+	max-records 30;
+	masters { 10.53.0.1; };
+	file "ixfr-too-big.bk";
+};
diff --git a/bin/tests/system/xfer/setup.sh b/bin/tests/system/xfer/setup.sh
index 56ca9018ec28..8f96b4e975e8 100644
--- a/bin/tests/system/xfer/setup.sh
+++ b/bin/tests/system/xfer/setup.sh
@@ -31,5 +31,7 @@ cp -f ns4/root.db.in ns4/root.db
 $PERL -e 'for ($i=0;$i<10000;$i++){ printf("x%u 0 in a 10.53.0.1\n", $i);}' >> ns4/root.db
 cp -f ns4/named.conf.base ns4/named.conf
 
+cp -f ns1/ixfr-too-big.db.in ns1/ixfr-too-big.db
+
 cp ns2/slave.db.in ns2/slave.db
 touch -t 200101010000 ns2/slave.db
diff --git a/bin/tests/system/xfer/tests.sh b/bin/tests/system/xfer/tests.sh
index 089b1c716fe9..89823c63e918 100644
--- a/bin/tests/system/xfer/tests.sh
+++ b/bin/tests/system/xfer/tests.sh
@@ -368,5 +368,31 @@ $DIGCMD nil. TXT | grep 'incorrect key AXFR' >/dev/null && {
     status=1
 }
 
+n=`expr $n + 1`
+echo "I:test that a zone with too many records is rejected (AXFR) ($n)"
+tmp=0
+grep "'axfr-too-big/IN'.*: too many records" ns6/named.run >/dev/null || tmp=1
+if test $tmp != 0 ; then echo "I:failed"; fi
+status=`expr $status + $tmp`
+
+n=`expr $n + 1`
+echo "I:test that a zone with too many records is rejected (IXFR) ($n)"
+tmp=0
+grep "'ixfr-too-big./IN.*: too many records" ns6/named.run >/dev/null && tmp=1
+$NSUPDATE << EOF
+zone ixfr-too-big
+server 10.53.0.1 5300
+update add the-31st-record.ixfr-too-big 0 TXT this is it
+send
+EOF
+for i in 1 2 3 4 5 6 7 8
+do
+    grep "'ixfr-too-big/IN'.*: too many records" ns6/named.run >/dev/null && break
+    sleep 1
+done
+grep "'ixfr-too-big/IN'.*: too many records" ns6/named.run >/dev/null || tmp=1
+if test $tmp != 0 ; then echo "I:failed"; fi
+status=`expr $status + $tmp`
+
 echo "I:exit status: $status"
 exit $status
diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml
index fe479b029fa3..5a45766c52fd 100644
--- a/doc/arm/Bv9ARM-book.xml
+++ b/doc/arm/Bv9ARM-book.xml
@@ -4374,6 +4374,7 @@ badresp:1,adberr:0,findfail:0,valfail:0]
     <optional> use-queryport-pool <replaceable>yes_or_no</replaceable>; </optional>
     <optional> queryport-pool-ports <replaceable>number</replaceable>; </optional>
     <optional> queryport-pool-updateinterval <replaceable>number</replaceable>; </optional>
+    <optional> max-records <replaceable>number</replaceable>; </optional>
     <optional> max-transfer-time-in <replaceable>number</replaceable>; </optional>
     <optional> max-transfer-time-out <replaceable>number</replaceable>; </optional>
     <optional> max-transfer-idle-in <replaceable>number</replaceable>; </optional>
@@ -7592,6 +7593,16 @@ avoid-v6-udp-ports { 40000; range 50000 60000; };
 	    </varlistentry>
 
 	    <varlistentry>
+	      <term><command>max-records</command></term>
+	      <listitem>
+		<para>
+		  The maximum number of records permitted in a zone.
+		  The default is zero which means unlimited.
+	 	</para>
+	      </listitem>
+	    </varlistentry>
+
+	    <varlistentry>
 	      <term><command>host-statistics-max</command></term>
 	      <listitem>
 		<para>
@@ -11140,6 +11151,16 @@ zone <replaceable>zone_name</replaceable> <optional><replaceable>class</replacea
 	      </varlistentry>
 
 	      <varlistentry>
+		<term><command>max-records</command></term>
+		<listitem>
+		  <para>
+		    See the description of
+		    <command>max-records</command> in <xref linkend="server_resource_limits"/>.
+		  </para>
+		</listitem>
+	      </varlistentry>
+
+	      <varlistentry>
 		<term><command>max-transfer-time-in</command></term>
 		<listitem>
 		  <para>
diff --git a/doc/arm/notes.xml b/doc/arm/notes.xml
index 66ebc8b99a2d..106c7bf1966b 100644
--- a/doc/arm/notes.xml
+++ b/doc/arm/notes.xml
@@ -45,7 +45,11 @@
     <itemizedlist>
       <listitem>
 	<para>
-	  None.
+	  Added the ability to specify the maximum number of records
+	  permitted in a zone (max-records #;).  This provides a mechanism
+	  to block overly large zone transfers, which is a potential risk
+	  with slave zones from other parties, as described in CVE-2016-6170.
+	  [RT #42143]
 	</para>
       </listitem>
     </itemizedlist>
diff --git a/lib/bind9/check.c b/lib/bind9/check.c
index cbfa8301e3f4..5d541dfb3621 100644
--- a/lib/bind9/check.c
+++ b/lib/bind9/check.c
@@ -1333,6 +1333,8 @@ check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions,
 	  REDIRECTZONE },
 	{ "masters", SLAVEZONE | STUBZONE | REDIRECTZONE },
 	{ "max-ixfr-log-size", MASTERZONE | SLAVEZONE | STREDIRECTZONE },
+	{ "max-records", MASTERZONE | SLAVEZONE | STUBZONE | STREDIRECTZONE |
+          STATICSTUBZONE | REDIRECTZONE },
 	{ "max-refresh-time", SLAVEZONE | STUBZONE | STREDIRECTZONE },
 	{ "max-retry-time", SLAVEZONE | STUBZONE | STREDIRECTZONE },
 	{ "max-transfer-idle-in", SLAVEZONE | STUBZONE | STREDIRECTZONE },
diff --git a/lib/dns/db.c b/lib/dns/db.c
index bf4a5b37540c..55deed7975b1 100644
--- a/lib/dns/db.c
+++ b/lib/dns/db.c
@@ -983,6 +983,19 @@ dns_db_getnsec3parameters(dns_db_t *db, dns_dbversion_t *version,
 }
 
 isc_result_t
+dns_db_getsize(dns_db_t *db, dns_dbversion_t *version, isc_uint64_t *records,
+	       isc_uint64_t *bytes)
+{
+	REQUIRE(DNS_DB_VALID(db));
+	REQUIRE(dns_db_iszone(db) == ISC_TRUE);
+
+	if (db->methods->getsize != NULL)
+		return ((db->methods->getsize)(db, version, records, bytes));
+
+	return (ISC_R_NOTFOUND);
+}
+
+isc_result_t
 dns_db_setsigningtime(dns_db_t *db, dns_rdataset_t *rdataset,
 		      isc_stdtime_t resign)
 {
diff --git a/lib/dns/ecdb.c b/lib/dns/ecdb.c
index 22cd810a773c..816489360fd3 100644
--- a/lib/dns/ecdb.c
+++ b/lib/dns/ecdb.c
@@ -583,7 +583,8 @@ static dns_dbmethods_t ecdb_methods = {
 	NULL,			/* rpz_enabled */
 	NULL,			/* rpz_findips */
 	NULL,			/* findnodeext */
-	NULL			/* findext */
+	NULL,			/* findext */
+	NULL			/* getsize */
 };
 
 static isc_result_t
diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h
index 66bc3e3481e1..dce6d1327581 100644
--- a/lib/dns/include/dns/db.h
+++ b/lib/dns/include/dns/db.h
@@ -194,6 +194,8 @@ typedef struct dns_dbmethods {
 				   dns_clientinfo_t *clientinfo,
 				   dns_rdataset_t *rdataset,
 				   dns_rdataset_t *sigrdataset);
+	isc_result_t	(*getsize)(dns_db_t *db, dns_dbversion_t *version,
+				   isc_uint64_t *records, isc_uint64_t *bytes);
 } dns_dbmethods_t;
 
 typedef isc_result_t
@@ -1445,6 +1447,24 @@ dns_db_getnsec3parameters(dns_db_t *db, dns_dbversion_t *version,
  */
 
 isc_result_t
+dns_db_getsize(dns_db_t *db, dns_dbversion_t *version, isc_uint64_t *records,
+               isc_uint64_t *bytes);
+/*%<
+ * Get the number of records in the given version of the database as well
+ * as the number bytes used to store those records.
+ *
+ * Requires:
+ * \li	'db' is a valid zone database.
+ * \li	'version' is NULL or a valid version.
+ * \li	'records' is NULL or a pointer to return the record count in.
+ * \li	'bytes' is NULL or a pointer to return the byte count in.
+ *
+ * Returns:
+ * \li	#ISC_R_SUCCESS
+ * \li	#ISC_R_NOTIMPLEMENTED
+ */
+
+isc_result_t
 dns_db_findnsec3node(dns_db_t *db, dns_name_t *name,
 		     isc_boolean_t create, dns_dbnode_t **nodep);
 /*%<
diff --git a/lib/dns/include/dns/rdataslab.h b/lib/dns/include/dns/rdataslab.h
index 3ac44b879e03..2e1e7592a28d 100644
--- a/lib/dns/include/dns/rdataslab.h
+++ b/lib/dns/include/dns/rdataslab.h
@@ -104,6 +104,7 @@ dns_rdataslab_tordataset(unsigned char *slab, unsigned int reservelen,
  * Ensures:
  *\li	'rdataset' is associated and points to a valid rdataest.
  */
+
 unsigned int
 dns_rdataslab_size(unsigned char *slab, unsigned int reservelen);
 /*%<
@@ -116,6 +117,18 @@ dns_rdataslab_size(unsigned char *slab, unsigned int reservelen);
  *\li	The number of bytes in the slab, including the reservelen.
  */
 
+unsigned int
+dns_rdataslab_count(unsigned char *slab, unsigned int reservelen);
+/*%<
+ * Return the number of records in the rdataslab
+ *
+ * Requires:
+ *\li	'slab' points to a slab.
+ *
+ * Returns:
+ *\li	The number of records in the slab.
+ */
+
 isc_result_t
 dns_rdataslab_merge(unsigned char *oslab, unsigned char *nslab,
 		    unsigned int reservelen, isc_mem_t *mctx,
diff --git a/lib/dns/include/dns/result.h b/lib/dns/include/dns/result.h
index 7d11c2beb01e..de38dba7b4b4 100644
--- a/lib/dns/include/dns/result.h
+++ b/lib/dns/include/dns/result.h
@@ -157,8 +157,9 @@
 #define DNS_R_BADCDS			(ISC_RESULTCLASS_DNS + 111)
 #define DNS_R_BADCDNSKEY		(ISC_RESULTCLASS_DNS + 112)
 #define DNS_R_OPTERR			(ISC_RESULTCLASS_DNS + 113)
+#define DNS_R_TOOMANYRECORDS		(ISC_RESULTCLASS_DNS + 114)
 
-#define DNS_R_NRESULTS			114	/*%< Number of results */
+#define DNS_R_NRESULTS			115	/*%< Number of results */
 
 /*
  * DNS wire format rcodes.
diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h
index 987d06f70cec..ec9434bc11b9 100644
--- a/lib/dns/include/dns/zone.h
+++ b/lib/dns/include/dns/zone.h
@@ -288,6 +288,31 @@ dns_zone_getfile(dns_zone_t *zone);
  * Returns:
  *\li	Pointer to null-terminated file name, or NULL.
  */
+void
+dns_zone_setmaxrecords(dns_zone_t *zone, isc_uint32_t records);
+/*%<
+ * 	Sets the maximim number of records permitted in a zone.
+ *	0 implies unlimited.
+ *
+ * Requires:
+ *\li	'zone' to be valid initialised zone.
+ *
+ * Returns:
+ *\li	void
+ */
+
+isc_uint32_t
+dns_zone_getmaxrecords(dns_zone_t *zone);
+/*%<
+ * 	Gets the maximim number of records permitted in a zone.
+ *	0 implies unlimited.
+ *
+ * Requires:
+ *\li	'zone' to be valid initialised zone.
+ *
+ * Returns:
+ *\li	isc_uint32_t maxrecords.
+ */
 
 isc_result_t
 dns_zone_load(dns_zone_t *zone);
diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c
index 80713da20dd9..45c635fe3efd 100644
--- a/lib/dns/rbtdb.c
+++ b/lib/dns/rbtdb.c
@@ -156,6 +156,7 @@ typedef isc_uint64_t                    rbtdb_serial_t;
 #define free_rbtdb_callback free_rbtdb_callback64
 #define free_rdataset free_rdataset64
 #define getnsec3parameters getnsec3parameters64
+#define getsize getsize64
 #define getoriginnode getoriginnode64
 #define getrrsetstats getrrsetstats64
 #define getsigningtime getsigningtime64
@@ -521,6 +522,13 @@ typedef struct rbtdb_version {
 	isc_uint16_t			iterations;
 	isc_uint8_t			salt_length;
 	unsigned char			salt[DNS_NSEC3_SALTSIZE];
+
+	/*
+	 * records and bytes are covered by rwlock.
+	 */
+	isc_rwlock_t                    rwlock;
+	isc_uint64_t			records;
+	isc_uint64_t			bytes;
 } rbtdb_version_t;
 
 typedef ISC_LIST(rbtdb_version_t)       rbtdb_versionlist_t;
@@ -993,6 +1001,7 @@ free_rbtdb(dns_rbtdb_t *rbtdb, isc_boolean_t log, isc_event_t *event) {
 		INSIST(refs == 0);
 		UNLINK(rbtdb->open_versions, rbtdb->current_version, link);
 		isc_refcount_destroy(&rbtdb->current_version->references);
+		isc_rwlock_destroy(&rbtdb->current_version->rwlock);
 		isc_mem_put(rbtdb->common.mctx, rbtdb->current_version,
 			    sizeof(rbtdb_version_t));
 	}
@@ -1231,6 +1240,7 @@ allocate_version(isc_mem_t *mctx, rbtdb_serial_t serial,
 
 static isc_result_t
 newversion(dns_db_t *db, dns_dbversion_t **versionp) {
+	isc_result_t result;
 	dns_rbtdb_t *rbtdb = (dns_rbtdb_t *)db;
 	rbtdb_version_t *version;
 
@@ -1263,13 +1273,28 @@ newversion(dns_db_t *db, dns_dbversion_t **versionp) {
 			version->salt_length = 0;
 			memset(version->salt, 0, sizeof(version->salt));
 		}
-		rbtdb->next_serial++;
-		rbtdb->future_version = version;
-	}
+		result = isc_rwlock_init(&version->rwlock, 0, 0);
+		if (result != ISC_R_SUCCESS) {
+			isc_refcount_destroy(&version->references);
+			isc_mem_put(rbtdb->common.mctx, version,
+				    sizeof(*version));
+			version = NULL;
+		} else {
+			RWLOCK(&rbtdb->current_version->rwlock,
+			       isc_rwlocktype_read);
+			version->records = rbtdb->current_version->records;
+			version->bytes = rbtdb->current_version->bytes;
+			RWUNLOCK(&rbtdb->current_version->rwlock,
+				 isc_rwlocktype_read);
+			rbtdb->next_serial++;
+			rbtdb->future_version = version;
+		}
+	} else
+		result = ISC_R_NOMEMORY;
 	RBTDB_UNLOCK(&rbtdb->lock, isc_rwlocktype_write);
 
 	if (version == NULL)
-		return (ISC_R_NOMEMORY);
+		return (result);
 
 	*versionp = version;
 
@@ -2509,6 +2534,7 @@ closeversion(dns_db_t *db, dns_dbversion_t **versionp, isc_boolean_t commit) {
 
 	if (cleanup_version != NULL) {
 		INSIST(EMPTY(cleanup_version->changed_list));
+		isc_rwlock_destroy(&cleanup_version->rwlock);
 		isc_mem_put(rbtdb->common.mctx, cleanup_version,
 			    sizeof(*cleanup_version));
 	}
@@ -6388,6 +6414,26 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion,
 		else
 			rbtnode->data = newheader;
 		newheader->next = topheader->next;
+		if (rbtversion != NULL)
+			RWLOCK(&rbtversion->rwlock, isc_rwlocktype_write);
+		if (rbtversion != NULL && !header_nx) {
+			rbtversion->records -=
+				dns_rdataslab_count((unsigned char *)header,
+						    sizeof(*header));
+			rbtversion->bytes -=
+				dns_rdataslab_size((unsigned char *)header,
+						   sizeof(*header));
+		}
+		if (rbtversion != NULL && !newheader_nx) {
+			rbtversion->records +=
+				dns_rdataslab_count((unsigned char *)newheader,
+						    sizeof(*newheader));
+			rbtversion->bytes +=
+				dns_rdataslab_size((unsigned char *)newheader,
+						   sizeof(*newheader));
+		}
+		if (rbtversion != NULL)
+			RWUNLOCK(&rbtversion->rwlock, isc_rwlocktype_write);
 		if (loading) {
 			/*
 			 * There are no other references to 'header' when
@@ -6490,6 +6536,16 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion,
 			newheader->down = NULL;
 			rbtnode->data = newheader;
 		}
+		if (rbtversion != NULL && !newheader_nx) {
+			RWLOCK(&rbtversion->rwlock, isc_rwlocktype_write);
+			rbtversion->records +=
+				dns_rdataslab_count((unsigned char *)newheader,
+						    sizeof(*newheader));
+			rbtversion->bytes +=
+				dns_rdataslab_size((unsigned char *)newheader,
+						   sizeof(*newheader));
+			RWUNLOCK(&rbtversion->rwlock, isc_rwlocktype_write);
+		}
 		idx = newheader->node->locknum;
 		if (IS_CACHE(rbtdb)) {
 			ISC_LIST_PREPEND(rbtdb->rdatasets[idx],
@@ -6944,6 +7000,12 @@ subtractrdataset(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version,
 			 */
 			newheader->additional_auth = NULL;
 			newheader->additional_glue = NULL;
+			rbtversion->records +=
+				dns_rdataslab_count((unsigned char *)newheader,
+						    sizeof(*newheader));
+			rbtversion->bytes +=
+				dns_rdataslab_size((unsigned char *)newheader,
+						   sizeof(*newheader));
 		} else if (result == DNS_R_NXRRSET) {
 			/*
 			 * This subtraction would remove all of the rdata;
@@ -6978,6 +7040,12 @@ subtractrdataset(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version,
 		 * topheader.
 		 */
 		INSIST(rbtversion->serial >= topheader->serial);
+		rbtversion->records -=
+				dns_rdataslab_count((unsigned char *)header,
+						    sizeof(*header));
+		rbtversion->bytes -=
+				dns_rdataslab_size((unsigned char *)header,
+						   sizeof(*header));
 		if (topheader_prev != NULL)
 			topheader_prev->next = newheader;
 		else
@@ -7509,6 +7577,33 @@ getnsec3parameters(dns_db_t *db, dns_dbversion_t *version, dns_hash_t *hash,
 }
 
 static isc_result_t
+getsize(dns_db_t *db, dns_dbversion_t *version, isc_uint64_t *records,
+        isc_uint64_t *bytes)
+{
+	dns_rbtdb_t *rbtdb;
+	isc_result_t result = ISC_R_SUCCESS;
+	rbtdb_version_t *rbtversion = version;
+
+	rbtdb = (dns_rbtdb_t *)db;
+
+	REQUIRE(VALID_RBTDB(rbtdb));
+	INSIST(rbtversion == NULL || rbtversion->rbtdb == rbtdb);
+
+	if (rbtversion == NULL)
+		rbtversion = rbtdb->current_version;
+
+	RWLOCK(&rbtversion->rwlock, isc_rwlocktype_read);
+	if (records != NULL)
+		*records = rbtversion->records;
+
+	if (bytes != NULL)
+		*bytes = rbtversion->bytes;
+	RWUNLOCK(&rbtversion->rwlock, isc_rwlocktype_read);
+
+	return (result);
+}
+
+static isc_result_t
 setsigningtime(dns_db_t *db, dns_rdataset_t *rdataset, isc_stdtime_t resign) {
 	dns_rbtdb_t *rbtdb = (dns_rbtdb_t *)db;
 	isc_stdtime_t oldresign;
@@ -7694,7 +7789,8 @@ static dns_dbmethods_t zone_methods = {
 	NULL,
 #endif
 	NULL,
-	NULL
+	NULL,
+	getsize
 };
 
 static dns_dbmethods_t cache_methods = {
@@ -7737,6 +7833,7 @@ static dns_dbmethods_t cache_methods = {
 	NULL,
 	NULL,
 	NULL,
+	NULL,
 	NULL
 };
 
@@ -8025,6 +8122,20 @@ dns_rbtdb_create
 	rbtdb->current_version->salt_length = 0;
 	memset(rbtdb->current_version->salt, 0,
 	       sizeof(rbtdb->current_version->salt));
+	result = isc_rwlock_init(&rbtdb->current_version->rwlock, 0, 0);
+	if (result != ISC_R_SUCCESS) {
+		isc_refcount_destroy(&rbtdb->current_version->references);
+		isc_mem_put(mctx, rbtdb->current_version,
+			    sizeof(*rbtdb->current_version));
+		rbtdb->current_version = NULL;
+		isc_refcount_decrement(&rbtdb->references, NULL);
+		isc_refcount_destroy(&rbtdb->references);
+		free_rbtdb(rbtdb, ISC_FALSE, NULL);
+		return (result);
+	}
+
+	rbtdb->current_version->records = 0;
+	rbtdb->current_version->bytes = 0;
 	rbtdb->future_version = NULL;
 	ISC_LIST_INIT(rbtdb->open_versions);
 	/*
diff --git a/lib/dns/rdataslab.c b/lib/dns/rdataslab.c
index 9367127d9eaf..4dd38443c77c 100644
--- a/lib/dns/rdataslab.c
+++ b/lib/dns/rdataslab.c
@@ -522,6 +522,19 @@ dns_rdataslab_size(unsigned char *slab, unsigned int reservelen) {
 	return ((unsigned int)(current - slab));
 }
 
+unsigned int
+dns_rdataslab_count(unsigned char *slab, unsigned int reservelen) {
+	unsigned int count;
+	unsigned char *current;
+
+	REQUIRE(slab != NULL);
+
+	current = slab + reservelen;
+	count = *current++ * 256;
+	count += *current++;
+	return (count);
+}
+
 /*
  * Make the dns_rdata_t 'rdata' refer to the slab item
  * beginning at '*current', which is part of a slab of type
diff --git a/lib/dns/result.c b/lib/dns/result.c
index 7be4f577ed86..f03bbe31601c 100644
--- a/lib/dns/result.c
+++ b/lib/dns/result.c
@@ -168,6 +168,7 @@ static const char *text[DNS_R_NRESULTS] = {
 	"bad CDS",			       /*%< 111 DNS_R_BADCSD */
 	"bad CDNSKEY",			       /*%< 112 DNS_R_BADCDNSKEY */
 	"malformed OPT option"		       /*%< 113 DNS_R_OPTERR */
+	"too many records",	               /*%< 114 DNS_R_TOOMANYRECORDS */
 };
 
 static const char *rcode_text[DNS_R_NRCODERESULTS] = {
diff --git a/lib/dns/sdb.c b/lib/dns/sdb.c
index f0ffc3d6f384..a0655ad1ee92 100644
--- a/lib/dns/sdb.c
+++ b/lib/dns/sdb.c
@@ -1296,7 +1296,8 @@ static dns_dbmethods_t sdb_methods = {
 	NULL,			/* rpz_enabled */
 	NULL,			/* rpz_findips */
 	findnodeext,
-	findext
+	findext,
+	NULL			/* getsize */
 };
 
 static isc_result_t
diff --git a/lib/dns/sdlz.c b/lib/dns/sdlz.c
index e70532021719..ff96af434933 100644
--- a/lib/dns/sdlz.c
+++ b/lib/dns/sdlz.c
@@ -1267,7 +1267,8 @@ static dns_dbmethods_t sdlzdb_methods = {
 	NULL,			/* rpz_enabled */
 	NULL,			/* rpz_findips */
 	findnodeext,
-	findext
+	findext,
+	NULL			/* getsize */
 };
 
 /*
diff --git a/lib/dns/xfrin.c b/lib/dns/xfrin.c
index aec238d3fc1a..e71e42dbac56 100644
--- a/lib/dns/xfrin.c
+++ b/lib/dns/xfrin.c
@@ -147,6 +147,9 @@ struct dns_xfrin_ctx {
 	unsigned int		nrecs;		/*%< Number of records recvd */
 	isc_uint64_t		nbytes;		/*%< Number of bytes received */
 
+	unsigned int		maxrecords;	/*%< The maximum number of
+						     records set for the zone */
+
 	isc_time_t		start;		/*%< Start time of the transfer */
 	isc_time_t		end;		/*%< End time of the transfer */
 
@@ -312,11 +315,19 @@ axfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
 static isc_result_t
 axfr_apply(dns_xfrin_ctx_t *xfr) {
 	isc_result_t result;
+	isc_uint64_t records;
 
 	CHECK(dns_diff_load(&xfr->diff,
 			    xfr->axfr.add_func, xfr->axfr.add_private));
 	xfr->difflen = 0;
 	dns_diff_clear(&xfr->diff);
+	if (xfr->maxrecords != 0U) {
+		result = dns_db_getsize(xfr->db, xfr->ver, &records, NULL);
+		if (result == ISC_R_SUCCESS && records > xfr->maxrecords) {
+			result = DNS_R_TOOMANYRECORDS;
+			goto failure;
+		}
+	}
 	result = ISC_R_SUCCESS;
  failure:
 	return (result);
@@ -403,6 +414,7 @@ ixfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
 static isc_result_t
 ixfr_apply(dns_xfrin_ctx_t *xfr) {
 	isc_result_t result;
+	isc_uint64_t records;
 
 	if (xfr->ver == NULL) {
 		CHECK(dns_db_newversion(xfr->db, &xfr->ver));
@@ -410,6 +422,13 @@ ixfr_apply(dns_xfrin_ctx_t *xfr) {
 			CHECK(dns_journal_begin_transaction(xfr->ixfr.journal));
 	}
 	CHECK(dns_diff_apply(&xfr->diff, xfr->db, xfr->ver));
+	if (xfr->maxrecords != 0U) {
+		result = dns_db_getsize(xfr->db, xfr->ver, &records, NULL);
+		if (result == ISC_R_SUCCESS && records > xfr->maxrecords) {
+			result = DNS_R_TOOMANYRECORDS;
+			goto failure;
+		}
+	}
 	if (xfr->ixfr.journal != NULL) {
 		result = dns_journal_writediff(xfr->ixfr.journal, &xfr->diff);
 		if (result != ISC_R_SUCCESS)
@@ -752,7 +771,7 @@ xfrin_reset(dns_xfrin_ctx_t *xfr) {
 
 static void
 xfrin_fail(dns_xfrin_ctx_t *xfr, isc_result_t result, const char *msg) {
-	if (result != DNS_R_UPTODATE) {
+	if (result != DNS_R_UPTODATE && result != DNS_R_TOOMANYRECORDS) {
 		xfrin_log(xfr, ISC_LOG_ERROR, "%s: %s",
 			  msg, isc_result_totext(result));
 		if (xfr->is_ixfr)
@@ -843,6 +862,7 @@ xfrin_create(isc_mem_t *mctx,
 	xfr->nmsg = 0;
 	xfr->nrecs = 0;
 	xfr->nbytes = 0;
+	xfr->maxrecords = dns_zone_getmaxrecords(zone);
 	isc_time_now(&xfr->start);
 
 	xfr->tsigkey = NULL;
diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index 490248a68910..c59ef3201ec6 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -248,6 +248,8 @@ struct dns_zone {
 	isc_uint32_t		maxretry;
 	isc_uint32_t		minretry;
 
+	isc_uint32_t		maxrecords;
+
 	isc_sockaddr_t		*masters;
 	dns_name_t		**masterkeynames;
 	isc_boolean_t		*mastersok;
@@ -9689,6 +9691,20 @@ dns_zone_setmaxretrytime(dns_zone_t *zone, isc_uint32_t val) {
 	zone->maxretry = val;
 }
 
+isc_uint32_t
+dns_zone_getmaxrecords(dns_zone_t *zone) {
+        REQUIRE(DNS_ZONE_VALID(zone));
+
+	return (zone->maxrecords);
+}
+
+void
+dns_zone_setmaxrecords(dns_zone_t *zone, isc_uint32_t val) {
+        REQUIRE(DNS_ZONE_VALID(zone));
+
+	zone->maxrecords = val;
+}
+
 static isc_boolean_t
 notify_isqueued(dns_zone_t *zone, unsigned int flags, dns_name_t *name,
 		isc_sockaddr_t *addr, dns_tsigkey_t *key)
@@ -13977,7 +13993,7 @@ zone_xfrdone(dns_zone_t *zone, isc_result_t result) {
 	DNS_ZONE_CLRFLAG(zone, DNS_ZONEFLG_SOABEFOREAXFR);
 
 	TIME_NOW(&now);
-	switch (result) {
+	switch (xfrresult) {
 	case ISC_R_SUCCESS:
 		DNS_ZONE_SETFLAG(zone, DNS_ZONEFLG_NEEDNOTIFY);
 		/*FALLTHROUGH*/
@@ -14104,6 +14120,11 @@ zone_xfrdone(dns_zone_t *zone, isc_result_t result) {
 		DNS_ZONE_SETFLAG(zone, DNS_ZONEFLAG_NOIXFR);
 		goto same_master;
 
+	case DNS_R_TOOMANYRECORDS:
+		DNS_ZONE_JITTER_ADD(&now, zone->refresh, &zone->refreshtime);
+		inc_stats(zone, dns_zonestatscounter_xfrfail);
+		break;
+
 	default:
 	next_master:
 		/*
diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c
index d95a03a61433..6f149602c05f 100644
--- a/lib/isccfg/namedconf.c
+++ b/lib/isccfg/namedconf.c
@@ -1597,6 +1597,7 @@ zone_clauses[] = {
 	{ "masterfile-format", &cfg_type_masterformat, 0 },
 	{ "max-ixfr-log-size", &cfg_type_size, CFG_CLAUSEFLAG_OBSOLETE },
 	{ "max-journal-size", &cfg_type_sizenodefault, 0 },
+	{ "max-records", &cfg_type_uint32, 0 },
 	{ "max-refresh-time", &cfg_type_uint32, 0 },
 	{ "max-retry-time", &cfg_type_uint32, 0 },
 	{ "max-transfer-idle-in", &cfg_type_uint32, 0 },
-- 
2.12.0