From 25818ac81fbc5665b101dead4b406a4dfe2d4486 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= <michal@isc.org>
Date: Fri, 10 Jul 2020 11:29:18 +0200
Subject: [PATCH] Fix locking for LMDB 0.9.26

When "rndc reconfig" is run, named first configures a fresh set of views
and then tears down the old views.  Consider what happens for a single
view with LMDB enabled; "envA" is the pointer to the LMDB environment
used by the original/old version of the view, "envB" is the pointer to
the same LMDB environment used by the new version of that view:

 1. mdb_env_open(envA) is called when the view is first created.
 2. "rndc reconfig" is called.
 3. mdb_env_open(envB) is called for the new instance of the view.
 4. mdb_env_close(envA) is called for the old instance of the view.

This seems to have worked so far.  However, an upstream change [1] in
LMDB which will be part of its 0.9.26 release prevents the above
sequence of calls from working as intended because the locktable mutexes
will now get destroyed by the mdb_env_close() call in step 4 above,
causing any subsequent mdb_txn_begin() calls to fail (because all of the
above steps are happening within a single named process).

Preventing the above scenario from happening would require either
redesigning the way we use LMDB in BIND, which is not something we can
easily backport, or redesigning the way BIND carries out its
reconfiguration process, which would be an even more severe change.

To work around the problem, set MDB_NOLOCK when calling mdb_env_open()
to stop LMDB from controlling concurrent access to the database and do
the necessary locking in named instead.  Reuse the view->new_zone_lock
mutex for this purpose to prevent the need for modifying struct dns_view
(which would necessitate library API version bumps).  Drop use of
MDB_NOTLS as it is made redundant by MDB_NOLOCK: MDB_NOTLS only affects
where LMDB reader locktable slots are stored while MDB_NOLOCK prevents
the reader locktable from being used altogether.

[1] https://git.openldap.org/openldap/openldap/-/commit/2fd44e325195ae81664eb5dc36e7d265927c5ebc

(cherry picked from commit 53120279b57e25b6462ef3ac4ef9c205a4e9192b)
Conflict: NA
Reference: https://gitlab.isc.org/isc-projects/bind9/-/commit/25818ac81fbc5665b101dead4b406a4dfe2d4486
---
 .gitlab-ci.yml             |  5 +---
 bin/named/server.c         | 61 +++++++++++++++++++++++++++++++++-----
 lib/dns/include/dns/view.h |  7 +----
 3 files changed, 55 insertions(+), 18 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea312c36de..bcea3c38e0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1099,8 +1099,6 @@ clang:freebsd11.4:amd64:
   variables:
     CFLAGS: "${CFLAGS_COMMON}"
     USER: gitlab-runner
-    # Temporarily disable LMDB support [GL #1976]
-    EXTRA_CONFIGURE: "--without-lmdb"
   <<: *freebsd_amd64
   <<: *build_job
 
@@ -1126,8 +1124,7 @@ unit:clang:freebsd11.4:amd64:
 clang:freebsd12.1:amd64:
   variables:
     CFLAGS: "${CFLAGS_COMMON}"
-    # Temporarily disable LMDB support [GL #1976]
-    EXTRA_CONFIGURE: "--enable-dnstap --without-lmdb"
+    EXTRA_CONFIGURE: "--enable-dnstap"
     USER: gitlab-runner
   <<: *freebsd_amd64
   <<: *build_job
diff --git a/bin/named/server.c b/bin/named/server.c
index a2fa2c864c..5fd9fc1176 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -6797,6 +6797,8 @@ count_newzones(dns_view_t *view, ns_cfgctx_t *nzcfg, int *num_zonesp) {
 		      "for view '%s'",
 		      view->new_zone_db, view->name);
 
+	LOCK(&view->new_zone_lock);
+
 	CHECK(nzd_count(view, &n));
 
 	*num_zonesp = n;
@@ -6811,6 +6813,8 @@ count_newzones(dns_view_t *view, ns_cfgctx_t *nzcfg, int *num_zonesp) {
 	if (result != ISC_R_SUCCESS)
 		*num_zonesp = 0;
 
+	UNLOCK(&view->new_zone_lock);
+
 	return (ISC_R_SUCCESS);
 }
 
@@ -7116,6 +7120,8 @@ typedef isc_result_t (*newzone_cfg_cb_t)(const cfg_obj_t *zconfig,
  * Immediately interrupt processing if an error is encountered while
  * transforming NZD data into a zone configuration object or if "callback"
  * returns an error.
+ *
+ * Caller must hold 'view->new_zone_lock'.
  */
 static isc_result_t
 for_all_newzone_cfgs(newzone_cfg_cb_t callback, cfg_obj_t *config,
@@ -7228,8 +7234,11 @@ configure_newzones(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig,
 		return (ISC_R_SUCCESS);
 	}
 
+	LOCK(&view->new_zone_lock);
+
 	result = nzd_open(view, MDB_RDONLY, &txn, &dbi);
 	if (result != ISC_R_SUCCESS) {
+		UNLOCK(&view->new_zone_lock);
 		return (ISC_R_SUCCESS);
 	}
 
@@ -7256,6 +7265,9 @@ configure_newzones(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig,
 	}
 
 	(void) nzd_close(&txn, false);
+
+	UNLOCK(&view->new_zone_lock);
+
 	return (result);
 }
 
@@ -7277,6 +7289,8 @@ get_newzone_config(dns_view_t *view, const char *zonename,
 
 	INSIST(zoneconfig != NULL && *zoneconfig == NULL);
 
+	LOCK(&view->new_zone_lock);
+
 	CHECK(nzd_open(view, MDB_RDONLY, &txn, &dbi));
 
 	isc_log_write(ns_g_lctx,
@@ -7310,6 +7324,8 @@ get_newzone_config(dns_view_t *view, const char *zonename,
  cleanup:
 	(void) nzd_close(&txn, false);
 
+	UNLOCK(&view->new_zone_lock);
+
 	if (zoneconf != NULL) {
 		cfg_obj_destroy(ns_g_addparser, &zoneconf);
 	}
@@ -11638,8 +11654,6 @@ nzd_save(MDB_txn **txnp, MDB_dbi dbi, dns_zone_t *zone,
 
 	nzd_setkey(&key, dns_zone_getorigin(zone), namebuf, sizeof(namebuf));
 
-	LOCK(&view->new_zone_lock);
-
 	if (zconfig == NULL) {
 		/* We're deleting the zone from the database */
 		status = mdb_del(*txnp, dbi, &key, NULL);
@@ -11739,8 +11753,6 @@ nzd_save(MDB_txn **txnp, MDB_dbi dbi, dns_zone_t *zone,
 	}
 	*txnp = NULL;
 
-	UNLOCK(&view->new_zone_lock);
-
 	if (text != NULL) {
 		isc_buffer_free(&text);
 	}
@@ -11748,6 +11760,11 @@ nzd_save(MDB_txn **txnp, MDB_dbi dbi, dns_zone_t *zone,
 	return (result);
 }
 
+/*
+ * Check whether the new zone database for 'view' can be opened for writing.
+ *
+ * Caller must hold 'view->new_zone_lock'.
+ */
 static isc_result_t
 nzd_writable(dns_view_t *view) {
 	isc_result_t result = ISC_R_SUCCESS;
@@ -11779,6 +11796,11 @@ nzd_writable(dns_view_t *view) {
 	return (result);
 }
 
+/*
+ * Open the new zone database for 'view' and start a transaction for it.
+ *
+ * Caller must hold 'view->new_zone_lock'.
+ */
 static isc_result_t
 nzd_open(dns_view_t *view, unsigned int flags, MDB_txn **txnp, MDB_dbi *dbi) {
 	int status;
@@ -11909,6 +11931,13 @@ nzd_env_reopen(dns_view_t *view) {
 	return (result);
 }
 
+/*
+ * If 'commit' is true, commit the new zone database transaction pointed to by
+ * 'txnp'; otherwise, abort that transaction.
+ *
+ * Caller must hold 'view->new_zone_lock' for the view that the transaction
+ * pointed to by 'txnp' was started for.
+ */
 static isc_result_t
 nzd_close(MDB_txn **txnp, bool commit) {
 	isc_result_t result = ISC_R_SUCCESS;
@@ -11931,6 +11960,12 @@ nzd_close(MDB_txn **txnp, bool commit) {
 	return (result);
 }
 
+/*
+ * Count the zones configured in the new zone database for 'view' and store the
+ * result in 'countp'.
+ *
+ * Caller must hold 'view->new_zone_lock'.
+ */
 static isc_result_t
 nzd_count(dns_view_t *view, int *countp) {
 	isc_result_t result;
@@ -11979,6 +12014,8 @@ migrate_nzf(dns_view_t *view) {
 	MDB_val key, data;
 	ns_dzarg_t dzarg;
 
+	LOCK(&view->new_zone_lock);
+
 	/*
 	 * If NZF file doesn't exist, or NZD DB exists and already
 	 * has data, return without attempting migration.
@@ -12122,6 +12159,8 @@ migrate_nzf(dns_view_t *view) {
 		result = nzd_close(&txn, commit);
 	}
 
+	UNLOCK(&view->new_zone_lock);
+
 	if (text != NULL) {
 		isc_buffer_free(&text);
 	}
@@ -12325,6 +12364,7 @@ do_addzone(ns_server_t *server, ns_cfgctx_t *cfg, dns_view_t *view,
 	MDB_dbi dbi;
 
 	UNUSED(zoneconf);
+	LOCK(&view->new_zone_lock);
 #endif /* HAVE_LMDB */
 
 	/* Zone shouldn't already exist */
@@ -12465,6 +12505,7 @@ do_addzone(ns_server_t *server, ns_cfgctx_t *cfg, dns_view_t *view,
 #else /* HAVE_LMDB */
 	if (txn != NULL)
 		(void) nzd_close(&txn, false);
+	UNLOCK(&view->new_zone_lock);
 #endif /* HAVE_LMDB */
 
 	if (zone != NULL)
@@ -12488,6 +12529,7 @@ do_modzone(ns_server_t *server, ns_cfgctx_t *cfg, dns_view_t *view,
 #else /* HAVE_LMDB */
 	MDB_txn *txn = NULL;
 	MDB_dbi dbi;
+	LOCK(&view->new_zone_lock);
 #endif /* HAVE_LMDB */
 
 	/* Zone must already exist */
@@ -12667,6 +12709,7 @@ do_modzone(ns_server_t *server, ns_cfgctx_t *cfg, dns_view_t *view,
 #else /* HAVE_LMDB */
 	if (txn != NULL)
 		(void) nzd_close(&txn, false);
+	UNLOCK(&view->new_zone_lock);
 #endif /* HAVE_LMDB */
 
 	if (zone != NULL)
@@ -12816,6 +12859,7 @@ rmzone(isc_task_t *task, isc_event_t *event) {
 	if (added && cfg != NULL) {
 #ifdef HAVE_LMDB
 		/* Make sure we can open the NZD database */
+		LOCK(&view->new_zone_lock);
 		result = nzd_open(view, 0, &txn, &dbi);
 		if (result != ISC_R_SUCCESS) {
 			isc_log_write(ns_g_lctx, NS_LOGCATEGORY_GENERAL,
@@ -12834,6 +12878,11 @@ rmzone(isc_task_t *task, isc_event_t *event) {
 				      "delete zone configuration: %s",
 				      isc_result_totext(result));
 		}
+
+		if (txn != NULL) {
+			(void)nzd_close(&txn, false);
+		}
+		UNLOCK(&view->new_zone_lock);
 #else
 		result = delete_zoneconf(view, cfg->add_parser,
 					 cfg->nzf_config,
@@ -12926,10 +12975,6 @@ rmzone(isc_task_t *task, isc_event_t *event) {
 		}
 	}
 
-#ifdef HAVE_LMDB
-	if (txn != NULL)
-		(void) nzd_close(&txn, false);
-#endif
 	if (raw != NULL)
 		dns_zone_detach(&raw);
 	dns_zone_detach(&zone);
diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h
index c849dec154..09a9725de1 100644
--- a/lib/dns/include/dns/view.h
+++ b/lib/dns/include/dns/view.h
@@ -240,12 +240,7 @@ struct dns_view {
 
 #ifdef HAVE_LMDB
 #include <lmdb.h>
-/*
- * MDB_NOTLS is used to prevent problems after configuration is reloaded, due
- * to the way LMDB's use of thread-local storage (TLS) interacts with the BIND9
- * thread model.
- */
-#define DNS_LMDB_COMMON_FLAGS		(MDB_CREATE | MDB_NOSUBDIR | MDB_NOTLS)
+#define DNS_LMDB_COMMON_FLAGS		(MDB_CREATE | MDB_NOSUBDIR | MDB_NOLOCK)
 #ifndef __OpenBSD__
 #define DNS_LMDB_FLAGS			(DNS_LMDB_COMMON_FLAGS)
 #else /* __OpenBSD__ */
-- 
2.23.0

