From 1bbcfe2fc84f57b1e4e075fb3bc2a1dd0a3a851f Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Wed, 2 Nov 2016 17:31:27 +1100 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] (cherry picked from commit 5f8412a4cb5ee14a0e8cddd4107854b40ee3291e) --- CHANGES | 4 + 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 + .../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 | 9 ++ 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 | 6 +- lib/dns/include/dns/zone.h | 28 +++- lib/dns/rbtdb.c | 127 +++++++++++++++++- lib/dns/rdataslab.c | 13 ++ lib/dns/result.c | 9 +- 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 + 34 files changed, 444 insertions(+), 15 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/CHANGES b/CHANGES index 52b8f9e723..cb18ffc77b 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,10 @@ 4505. [port] Use IP_PMTUDISC_OMIT if available. [RT #35494] +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] + 4503. [cleanup] "make uninstall" now removes files installed by BIND. (This currently excludes Python files due to lack of support in setup.py.) [RT #42912] diff --git a/bin/named/config.c b/bin/named/config.c index 6e7d4cf319..8f5c7f1b13 100644 --- a/bin/named/config.c +++ b/bin/named/config.c @@ -212,6 +212,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 dbbfa38369..66dee4cc1f 100644 --- a/bin/named/named.conf.docbook +++ b/bin/named/named.conf.docbook @@ -342,6 +342,7 @@ options { }; max-journal-size size_no_default; + max-records integer; max-transfer-time-in integer; max-transfer-time-out integer; max-transfer-idle-in integer; @@ -539,6 +540,7 @@ view string optional_class }; max-journal-size size_no_default; + max-records integer; max-transfer-time-in integer; max-transfer-time-out integer; max-transfer-idle-in integer; @@ -636,6 +638,7 @@ zone string optional_class }; max-journal-size size_no_default; + max-records integer; max-transfer-time-in integer; max-transfer-time-out integer; max-transfer-idle-in integer; diff --git a/bin/named/update.c b/bin/named/update.c index 83b1a05efd..cc2a611dbd 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) { isc_boolean_t had_dnskey; dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); dns_ttl_t maxttl = 0; + isc_uint32_t maxrecords; + isc_uint64_t records; INSIST(event->ev_type == DNS_EVENT_UPDATE); @@ -3138,6 +3140,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 85dee041dd..50c3b6103f 100644 --- a/bin/named/zoneconf.c +++ b/bin/named/zoneconf.c @@ -983,6 +983,13 @@ ns_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig, dns_zone_setmaxttl(raw, maxttl); } + 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 1212ae9c34..4cbb7efbbb 100644 --- a/bin/tests/system/nsupdate/clean.sh +++ b/bin/tests/system/nsupdate/clean.sh @@ -41,5 +41,6 @@ rm -f ns3/dsset-* rm -f ns3/example.db rm -f ns3/many.test.bk rm -f ns3/nsec3param.test.db +rm -f ns3/too-big.test.db rm -f nsupdate.out* rm -f typelist.out.* diff --git a/bin/tests/system/nsupdate/ns3/named.conf b/bin/tests/system/nsupdate/ns3/named.conf index 5ee7279048..bc69a677b3 100644 --- a/bin/tests/system/nsupdate/ns3/named.conf +++ b/bin/tests/system/nsupdate/ns3/named.conf @@ -66,3 +66,10 @@ zone "delegation.test" { allow-update { any; }; file "delegation.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 0000000000..7ff1e4a514 --- /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 828255ee53..43c40947de 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 c570ed1b04..e830efd907 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -620,5 +620,20 @@ $DIG +tcp @10.53.0.3 -p 5300 ns child.delegation.test > dig.out.ns1.test$n grep "status: NXDOMAIN" dig.out.ns1.test$n > /dev/null 2>&1 || ret=1 [ $ret = 0 ] || { echo I:failed; status=1; } +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" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/xfer/clean.sh b/bin/tests/system/xfer/clean.sh index 822d77c7f5..ceb01d087c 100644 --- a/bin/tests/system/xfer/clean.sh +++ b/bin/tests/system/xfer/clean.sh @@ -39,3 +39,4 @@ rm -f */ans.run rm -f ns2/mapped.db rm -f ns3/mapped.bk rm -f dig.out.?.* +rm -f ns1/ixfr-too-big.db ns1/ixfr-too-big.db.jnl 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 0000000000..d43760d9a8 --- /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 0000000000..318bb772af --- /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 07dad85d9b..1d292924c0 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 c9421b1f65..a12a92c2f6 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 d42fdd6ab9..4d3afd18fe 100644 --- a/bin/tests/system/xfer/setup.sh +++ b/bin/tests/system/xfer/setup.sh @@ -35,3 +35,5 @@ cp ns2/slave.db.in ns2/slave.db touch -t 200101010000 ns2/slave.db cp ns2/mapped.db.in ns2/mapped.db + +cp -f ns1/ixfr-too-big.db.in ns1/ixfr-too-big.db diff --git a/bin/tests/system/xfer/tests.sh b/bin/tests/system/xfer/tests.sh index dcc52b6a50..163cb29c2a 100644 --- a/bin/tests/system/xfer/tests.sh +++ b/bin/tests/system/xfer/tests.sh @@ -391,5 +391,31 @@ $PERL ../digcomp.pl knowngood.mapped dig.out.3.$n || 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 (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" [ $status -eq 0 ] || exit 1 diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml index d98b217b8d..0198dc006b 100644 --- a/doc/arm/Bv9ARM-book.xml +++ b/doc/arm/Bv9ARM-book.xml @@ -4462,6 +4462,7 @@ badresp:1,adberr:0,findfail:0,valfail:0] use-queryport-pool yes_or_no; queryport-pool-ports number; queryport-pool-updateinterval number; + max-records number; max-transfer-time-in number; max-transfer-time-out number; max-transfer-idle-in number; @@ -7882,6 +7883,16 @@ avoid-v6-udp-ports { 40000; range 50000 60000; }; + + max-records + + + The maximum number of records permitted in a zone. + The default is zero which means unlimited. + + + + host-statistics-max @@ -11761,6 +11772,16 @@ zone zone_name class + + max-records + + + See the description of + max-records in . + + + + max-transfer-time-in diff --git a/doc/arm/notes.xml b/doc/arm/notes.xml index 3150807116..08fab7f30c 100644 --- a/doc/arm/notes.xml +++ b/doc/arm/notes.xml @@ -41,6 +41,15 @@
Security Fixes + + + 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] + + It was possible to trigger a assertion when rendering a diff --git a/lib/bind9/check.c b/lib/bind9/check.c index 9933433c41..c08e0a2b27 100644 --- a/lib/bind9/check.c +++ b/lib/bind9/check.c @@ -1647,6 +1647,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 4193b2f506..3d5076cb59 100644 --- a/lib/dns/db.c +++ b/lib/dns/db.c @@ -998,6 +998,19 @@ dns_db_getnsec3parameters(dns_db_t *db, dns_dbversion_t *version, return (ISC_R_NOTFOUND); } +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 553a339bb5..b5d04d2f8d 100644 --- a/lib/dns/ecdb.c +++ b/lib/dns/ecdb.c @@ -587,7 +587,8 @@ static dns_dbmethods_t ecdb_methods = { NULL, /* findnodeext */ NULL, /* findext */ NULL, /* setcachestats */ - NULL /* hashsize */ + NULL, /* hashsize */ + NULL /* getsize */ }; static isc_result_t diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h index 58f32f13ab..7731fb327c 100644 --- a/lib/dns/include/dns/db.h +++ b/lib/dns/include/dns/db.h @@ -195,6 +195,8 @@ typedef struct dns_dbmethods { dns_rdataset_t *sigrdataset); isc_result_t (*setcachestats)(dns_db_t *db, isc_stats_t *stats); size_t (*hashsize)(dns_db_t *db); + 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 @@ -1484,6 +1486,24 @@ dns_db_getnsec3parameters(dns_db_t *db, dns_dbversion_t *version, * or this zone does not have NSEC3 records. */ +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 3ac44b879e..2e1e7592a2 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 7d11c2beb0..93d1fd53f7 100644 --- a/lib/dns/include/dns/result.h +++ b/lib/dns/include/dns/result.h @@ -157,8 +157,12 @@ #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_BADDNSTAP (ISC_RESULTCLASS_DNS + 114) +#define DNS_R_BADTSIG (ISC_RESULTCLASS_DNS + 115) +#define DNS_R_BADSIG0 (ISC_RESULTCLASS_DNS + 116) +#define DNS_R_TOOMANYRECORDS (ISC_RESULTCLASS_DNS + 117) -#define DNS_R_NRESULTS 114 /*%< Number of results */ +#define DNS_R_NRESULTS 118 /*%< Number of results */ /* * DNS wire format rcodes. diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index e959bc07ec..9228fa6bd3 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -295,6 +295,32 @@ dns_zone_getfile(dns_zone_t *zone); *\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. + */ + void dns_zone_setmaxttl(dns_zone_t *zone, isc_uint32_t maxttl); /*%< @@ -316,7 +342,7 @@ dns_zone_getmaxttl(dns_zone_t *zone); *\li 'zone' to be valid initialised zone. * * Returns: - *\li isc_uint32_t maxttl. + *\li dns_ttl_t maxttl. */ isc_result_t diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c index 72e0cc376c..68181c63bb 100644 --- a/lib/dns/rbtdb.c +++ b/lib/dns/rbtdb.c @@ -210,6 +210,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 @@ -601,6 +602,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; @@ -1143,6 +1151,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)); } @@ -1396,6 +1405,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; @@ -1428,13 +1438,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; @@ -2714,6 +2739,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)); } @@ -6295,6 +6321,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 @@ -6396,6 +6442,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], @@ -6862,6 +6918,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; @@ -6898,6 +6960,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 @@ -7229,6 +7297,7 @@ rbt_datafixer(dns_rbtnode_t *rbtnode, void *base, size_t filesize, unsigned char *limit = ((unsigned char *) base) + filesize; unsigned char *p; size_t size; + unsigned int count; REQUIRE(rbtnode != NULL); @@ -7236,6 +7305,9 @@ rbt_datafixer(dns_rbtnode_t *rbtnode, void *base, size_t filesize, p = (unsigned char *) header; size = dns_rdataslab_size(p, sizeof(*header)); + count = dns_rdataslab_count(p, sizeof(*header));; + rbtdb->current_version->records += count; + rbtdb->current_version->bytes += size; isc_crc64_update(crc, p, size); #ifdef DEBUG hexdump("hashing header", p, sizeof(rdatasetheader_t)); @@ -7851,6 +7923,33 @@ getnsec3parameters(dns_db_t *db, dns_dbversion_t *version, dns_hash_t *hash, return (result); } +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; @@ -8048,7 +8147,8 @@ static dns_dbmethods_t zone_methods = { NULL, NULL, NULL, - hashsize + hashsize, + getsize }; static dns_dbmethods_t cache_methods = { @@ -8094,7 +8194,8 @@ static dns_dbmethods_t cache_methods = { NULL, NULL, setcachestats, - hashsize + hashsize, + NULL }; isc_result_t @@ -8386,6 +8487,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 e29dc8415a..63e3728fbb 100644 --- a/lib/dns/rdataslab.c +++ b/lib/dns/rdataslab.c @@ -523,6 +523,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 7be4f577ed..a6219098f6 100644 --- a/lib/dns/result.c +++ b/lib/dns/result.c @@ -167,11 +167,16 @@ static const char *text[DNS_R_NRESULTS] = { "covered by negative trust anchor", /*%< 110 DNS_R_NTACOVERED */ "bad CDS", /*%< 111 DNS_R_BADCSD */ "bad CDNSKEY", /*%< 112 DNS_R_BADCDNSKEY */ - "malformed OPT option" /*%< 113 DNS_R_OPTERR */ + "malformed OPT option", /*%< 113 DNS_R_OPTERR */ + "malformed DNSTAP data", /*%< 114 DNS_R_BADDNSTAP */ + + "TSIG in wrong location", /*%< 115 DNS_R_BADTSIG */ + "SIG(0) in wrong location", /*%< 116 DNS_R_BADSIG0 */ + "too many records", /*%< 117 DNS_R_TOOMANYRECORDS */ }; static const char *rcode_text[DNS_R_NRCODERESULTS] = { - "NOERROR", /*%< 0 DNS_R_NOEROR */ + "NOERROR", /*%< 0 DNS_R_NOERROR */ "FORMERR", /*%< 1 DNS_R_FORMERR */ "SERVFAIL", /*%< 2 DNS_R_SERVFAIL */ "NXDOMAIN", /*%< 3 DNS_R_NXDOMAIN */ diff --git a/lib/dns/sdb.c b/lib/dns/sdb.c index abfeeb074f..19397e0aef 100644 --- a/lib/dns/sdb.c +++ b/lib/dns/sdb.c @@ -1298,7 +1298,8 @@ static dns_dbmethods_t sdb_methods = { findnodeext, findext, NULL, /* setcachestats */ - NULL /* hashsize */ + NULL, /* hashsize */ + NULL /* getsize */ }; static isc_result_t diff --git a/lib/dns/sdlz.c b/lib/dns/sdlz.c index b1198a43bb..0e3163dc25 100644 --- a/lib/dns/sdlz.c +++ b/lib/dns/sdlz.c @@ -1269,7 +1269,8 @@ static dns_dbmethods_t sdlzdb_methods = { findnodeext, findext, NULL, /* setcachestats */ - NULL /* hashsize */ + NULL, /* hashsize */ + NULL /* getsize */ }; /* diff --git a/lib/dns/xfrin.c b/lib/dns/xfrin.c index 592c1c4b9c..306c34ae60 100644 --- a/lib/dns/xfrin.c +++ b/lib/dns/xfrin.c @@ -149,6 +149,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,10 +315,18 @@ 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, 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); @@ -402,6 +413,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)); @@ -409,6 +421,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) @@ -765,7 +784,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) @@ -858,6 +877,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 e5b496ee5b..d9aebd9bf0 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -253,6 +253,8 @@ struct dns_zone { isc_uint32_t maxretry; isc_uint32_t minretry; + isc_uint32_t maxrecords; + isc_sockaddr_t *masters; isc_dscp_t *masterdscps; dns_name_t **masterkeynames; @@ -10118,6 +10120,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) @@ -14469,7 +14485,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*/ @@ -14596,6 +14612,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 912390ee9d..e11dd05cb8 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -1693,6 +1693,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 },