Commit 821a497d55 for bind

commit 821a497d55449d62e013713eaf1019a04c7fd605
Author: Alessio Podda <alessio@isc.org>
Date:   Sat Sep 12 01:21:32 2026 +0200

    Do not link zones into the zonemgr

    Previously zones were part of a linked list owned by the zonemgr
    that was used to print statistics. This commit instead walks the
    view list and the zone table for each view.

    As a side effect, it also makes the zonemgr explicitely a global
    singleton. Previously, only one instance existed but it was
    reference counted.

diff --git a/bin/named/include/named/server.h b/bin/named/include/named/server.h
index 3268779d63..5f6bafa432 100644
--- a/bin/named/include/named/server.h
+++ b/bin/named/include/named/server.h
@@ -128,6 +128,15 @@ named_server_destroy(named_server_t **serverp);
  * Destroy a server object, freeing its memory.
  */

+isc_result_t
+named_server_applyzones(named_server_t *server,
+			isc_result_t (*action)(dns_zone_t *, void *),
+			void *arg);
+/*%<
+ * Apply 'action' to every active zone owned by the server's current views,
+ * including raw, managed-keys, and redirect zones.
+ */
+
 void
 named_server_reloadwanted(void *arg, int signum);
 /*%<
diff --git a/bin/named/server.c b/bin/named/server.c
index 8e53d71e85..5122ec96a6 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -1735,7 +1735,6 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) {
 	dns_rdataclass_t zclass = view->rdclass;

 	dns_zone_setclass(zone, zclass);
-	RETERR(dns_zonemgr_managezone(named_g_server->zonemgr, zone));

 	dns_zone_setstats(zone, named_g_server->zonestats);

@@ -1791,10 +1790,9 @@ dns64_reverse(dns_view_t *view, isc_mem_t *mctx, isc_netaddr_t *na,
 	isc_buffer_constinit(&b, reverse, strlen(reverse));
 	isc_buffer_add(&b, strlen(reverse));
 	CHECK(dns_name_fromtext(name, &b, dns_rootname, 0));
-	dns_zone_create(&zone, mctx, 0);
+	CHECK(dns_zonemgr_createzone(named_g_server->zonemgr, &zone));
 	dns_zone_setorigin(zone, name);
 	dns_zone_setview(zone, view);
-	CHECK(dns_zonemgr_managezone(named_g_server->zonemgr, zone));
 	dns_zone_setclass(zone, view->rdclass);
 	dns_zone_settype(zone, dns_zone_primary);
 	dns_zone_setstats(zone, named_g_server->zonestats);
@@ -3166,7 +3164,6 @@ create_empty_zone(dns_zone_t *pzone, dns_name_t *name, dns_view_t *view,
 	if (pzone == NULL) {
 		CHECK(dns_zonemgr_createzone(named_g_server->zonemgr, &zone));
 		dns_zone_setorigin(zone, name);
-		CHECK(dns_zonemgr_managezone(named_g_server->zonemgr, zone));
 		if (db == NULL) {
 			dns_zone_setdbtype(zone, empty_dbtypec, empty_dbtype);
 		}
@@ -3237,7 +3234,7 @@ cleanup:

 static isc_result_t
 create_ipv4only_zone(dns_zone_t *pzone, dns_view_t *view,
-		     const dns_name_t *name, const char *type, isc_mem_t *mctx,
+		     const dns_name_t *name, const char *type,
 		     const char *server, const char *contact) {
 	char namebuf[DNS_NAME_FORMATSIZE];
 	const char *dbtype[4] = { "_builtin", NULL, "@", "." };
@@ -3273,9 +3270,8 @@ create_ipv4only_zone(dns_zone_t *pzone, dns_view_t *view,
 		/*
 		 * Create the actual zone.
 		 */
-		dns_zone_create(&zone, mctx, 0);
+		CHECK(dns_zonemgr_createzone(named_g_server->zonemgr, &zone));
 		dns_zone_setorigin(zone, name);
-		CHECK(dns_zonemgr_managezone(named_g_server->zonemgr, zone));
 		dns_zone_setclass(zone, view->rdclass);
 		dns_zone_settype(zone, dns_zone_primary);
 		dns_zone_setstats(zone, named_g_server->zonestats);
@@ -5371,7 +5367,7 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config,

 			CHECK(create_ipv4only_zone(zone, view, name,
 						   zones[ipv4only_zone].type,
-						   mctx, server, contact));
+						   server, contact));
 			if (zone != NULL) {
 				dns_zone_detach(&zone);
 			}
@@ -6124,8 +6120,6 @@ configure_zone(const cfg_obj_t *config, const cfg_obj_t *zconfig,
 						     &zone));
 			dns_zone_setorigin(zone, origin);
 			dns_zone_setview(zone, view);
-			CHECK(dns_zonemgr_managezone(named_g_server->zonemgr,
-						     zone));
 			dns_zone_setstats(zone, named_g_server->zonestats);
 		}
 		CHECK(named_zone_configure(config, vconfig, zconfig, aclctx,
@@ -6226,7 +6220,6 @@ configure_zone(const cfg_obj_t *config, const cfg_obj_t *zconfig,
 		CHECK(dns_zonemgr_createzone(named_g_server->zonemgr, &zone));
 		dns_zone_setorigin(zone, origin);
 		dns_zone_setview(zone, view);
-		CHECK(dns_zonemgr_managezone(named_g_server->zonemgr, zone));
 		dns_zone_setstats(zone, named_g_server->zonestats);
 	}
 	if (rpz_num != DNS_RPZ_INVALID_NUM) {
@@ -6403,8 +6396,6 @@ add_keydata_zone(dns_view_t *view, const char *directory, isc_mem_t *mctx) {
 	dns_zone_settype(zone, dns_zone_key);
 	dns_zone_setclass(zone, view->rdclass);

-	CHECK(dns_zonemgr_managezone(named_g_server->zonemgr, zone));
-
 	dns_acl_none(mctx, &none);
 	dns_zone_setqueryacl(zone, none);
 	dns_zone_setqueryonacl(zone, none);
@@ -8995,6 +8986,9 @@ cleanup:
 	return result;
 }

+static isc_result_t
+force_zone_maintenance(dns_zone_t *zone, void *arg);
+
 static void
 destroy_zoneload(zoneload_t *zl) {
 	isc_result_t result;
@@ -9040,8 +9034,10 @@ destroy_zoneload(zoneload_t *zl) {
 	 * so that we know when we need to force AXFR of
 	 * secondary zones whose master files are missing.
 	 */
-	CHECKFATAL(dns_zonemgr_forcemaint(server->zonemgr),
-		   "forcing zone maintenance");
+	CHECKFATAL(
+		named_server_applyzones(server, force_zone_maintenance, NULL),
+		"forcing zone maintenance");
+	dns_zonemgr_resumexfrs(server->zonemgr);

 	named_os_started();

@@ -9059,6 +9055,100 @@ destroy_zoneload(zoneload_t *zl) {
 	isc_log_write(NAMED_LOGCATEGORY_GENERAL, NAMED_LOGMODULE_SERVER,
 		      ISC_LOG_NOTICE, "running");
 }
+
+typedef struct applyzones_arg {
+	dns_view_t *view;
+	dns_zone_t *managed_keys;
+	dns_zone_t *redirect;
+	isc_result_t (*action)(dns_zone_t *, void *);
+	void *arg;
+} applyzones_arg_t;
+
+static isc_result_t
+apply_zone_and_raw(dns_zone_t *zone, applyzones_arg_t *aza) {
+	dns_zone_t *raw = NULL;
+	isc_result_t result = ISC_R_SUCCESS;
+
+	RETERR(aza->action(zone, aza->arg));
+
+	dns_zone_getraw(zone, &raw);
+	if (raw != NULL) {
+		result = aza->action(raw, aza->arg);
+		dns_zone_detach(&raw);
+	}
+	return result;
+}
+
+static isc_result_t
+apply_view_zone(dns_zone_t *zone, void *arg) {
+	applyzones_arg_t *aza = arg;
+
+	/* In-view zones may also be mounted in another view's table. */
+	if (dns_zone_getview(zone) != aza->view || zone == aza->managed_keys ||
+	    zone == aza->redirect)
+	{
+		return ISC_R_SUCCESS;
+	}
+
+	return apply_zone_and_raw(zone, aza);
+}
+
+isc_result_t
+named_server_applyzones(named_server_t *server,
+			isc_result_t (*action)(dns_zone_t *, void *),
+			void *arg) {
+	REQUIRE(NAMED_SERVER_VALID(server));
+	REQUIRE(action != NULL);
+
+	ISC_LIST_FOREACH(server->viewlist, view, link) {
+		applyzones_arg_t aza = {
+			.view = view,
+			.managed_keys = view->managed_keys,
+			.redirect = view->redirect,
+			.action = action,
+			.arg = arg,
+		};
+		RETERR(dns_view_apply(view, true, NULL, apply_view_zone, &aza));
+		if (view->managed_keys != NULL &&
+		    dns_zone_getview(view->managed_keys) == view)
+		{
+			RETERR(apply_zone_and_raw(view->managed_keys, &aza));
+		}
+		if (view->redirect != NULL &&
+		    dns_zone_getview(view->redirect) == view &&
+		    view->redirect != view->managed_keys)
+		{
+			RETERR(apply_zone_and_raw(view->redirect, &aza));
+		}
+	}
+
+	return ISC_R_SUCCESS;
+}
+
+typedef struct zonecounts {
+	unsigned int any;
+	unsigned int xferfirstrefresh;
+	unsigned int soaquery;
+	unsigned int automatic;
+} zonecounts_t;
+
+static isc_result_t
+count_zone_states(dns_zone_t *zone, void *arg) {
+	zonecounts_t *counts = arg;
+
+	counts->any += dns_zone_getcount(zone, DNS_ZONESTATE_ANY);
+	counts->xferfirstrefresh +=
+		dns_zone_getcount(zone, DNS_ZONESTATE_XFERFIRSTREFRESH);
+	counts->soaquery += dns_zone_getcount(zone, DNS_ZONESTATE_SOAQUERY);
+	counts->automatic += dns_zone_getcount(zone, DNS_ZONESTATE_AUTOMATIC);
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+force_zone_maintenance(dns_zone_t *zone, void *arg ISC_ATTR_UNUSED) {
+	dns_zone_forcemaint(zone);
+	return ISC_R_SUCCESS;
+}
 ISC_REFCOUNT_STATIC_IMPL(zoneload, destroy_zoneload);

 static isc_result_t
@@ -9573,7 +9663,8 @@ named_server_destroy(named_server_t **serverp) {
 	clearhostname(server);

 	if (server->zonemgr != NULL) {
-		dns_zonemgr_detach(&server->zonemgr);
+		/* All zone activity has drained before server destruction. */
+		dns_zonemgr_destroy(&server->zonemgr);
 	}

 	INSIST(ISC_LIST_EMPTY(server->kasplist));
@@ -11625,6 +11716,7 @@ named_server_status(named_server_t *server, isc_buffer_t *text) {
 	char configtime[ISC_FORMATHTTPTIMESTAMP_SIZE];
 	char line[1024], hostname[256];
 	named_reload_t reload_status;
+	zonecounts_t zonecounts = { 0 };

 	REQUIRE(text != NULL);

@@ -11637,17 +11729,15 @@ named_server_status(named_server_t *server, isc_buffer_t *text) {
 			alt = (char *)named_g_server->version.base;
 		}
 	}
-	zonecount = dns_zonemgr_getcount(server->zonemgr, DNS_ZONESTATE_ANY);
+	CHECK(named_server_applyzones(server, count_zone_states, &zonecounts));
+	zonecount = zonecounts.any;
 	xferrunning = dns_zonemgr_getcount(server->zonemgr,
 					   DNS_ZONESTATE_XFERRUNNING);
 	xferdeferred = dns_zonemgr_getcount(server->zonemgr,
 					    DNS_ZONESTATE_XFERDEFERRED);
-	xferfirstrefresh = dns_zonemgr_getcount(server->zonemgr,
-						DNS_ZONESTATE_XFERFIRSTREFRESH);
-	soaqueries = dns_zonemgr_getcount(server->zonemgr,
-					  DNS_ZONESTATE_SOAQUERY);
-	automatic = dns_zonemgr_getcount(server->zonemgr,
-					 DNS_ZONESTATE_AUTOMATIC);
+	xferfirstrefresh = zonecounts.xferfirstrefresh;
+	soaqueries = zonecounts.soaquery;
+	automatic = zonecounts.automatic;

 	isc_time_formathttptimestamp(&named_g_boottime, boottime,
 				     sizeof(boottime));
diff --git a/bin/named/statschannel.c b/bin/named/statschannel.c
index f5a05e7e24..ea1a1ecb92 100644
--- a/bin/named/statschannel.c
+++ b/bin/named/statschannel.c
@@ -4133,17 +4133,66 @@ named_statschannels_shutdown(named_server_t *server) {
 	}
 }

+static isc_result_t
+dump_zone_query_stats(dns_zone_t *zone, void *arg) {
+	FILE *fp = arg;
+	isc_stats_t *zonestats;
+	dns_view_t *view;
+	char zonename[DNS_NAME_FORMATSIZE];
+	uint64_t values[ns_statscounter_max];
+
+	if (dns_zone_getstatlevel(zone) != dns_zonestat_full ||
+	    (zonestats = dns_zone_getrequeststats(zone)) == NULL ||
+	    (view = dns_zone_getview(zone)) == NULL)
+	{
+		return ISC_R_SUCCESS;
+	}
+	dns_name_format(dns_zone_getorigin(zone), zonename, sizeof(zonename));
+	fprintf(fp, "[%s", zonename);
+	if (strcmp(view->name, "_default") != 0) {
+		fprintf(fp, " (view: %s)", view->name);
+	}
+	fprintf(fp, "]\n");
+	(void)dump_stats(zonestats, isc_statsformat_file, fp, NULL,
+			 nsstats_desc, ns_statscounter_max, nsstats_index,
+			 values, 0);
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+dump_zone_glue_stats(dns_zone_t *zone, void *arg) {
+	FILE *fp = arg;
+	isc_stats_t *gluecachestats;
+	dns_view_t *view;
+	char zonename[DNS_NAME_FORMATSIZE];
+	uint64_t values[dns_gluecachestatscounter_max];
+
+	if (dns_zone_getstatlevel(zone) != dns_zonestat_full ||
+	    (gluecachestats = dns_zone_getgluecachestats(zone)) == NULL ||
+	    (view = dns_zone_getview(zone)) == NULL)
+	{
+		return ISC_R_SUCCESS;
+	}
+	dns_name_format(dns_zone_getorigin(zone), zonename, sizeof(zonename));
+	fprintf(fp, "[%s", zonename);
+	if (strcmp(view->name, "_default") != 0) {
+		fprintf(fp, " (view: %s)", view->name);
+	}
+	fprintf(fp, "]\n");
+	(void)dump_stats(gluecachestats, isc_statsformat_file, fp, NULL,
+			 gluecachestats_desc, dns_gluecachestatscounter_max,
+			 gluecachestats_index, values, 0);
+	return ISC_R_SUCCESS;
+}
+
 isc_result_t
 named_stats_dump(named_server_t *server, FILE *fp) {
-	isc_result_t result;
-	dns_zone_t *zone, *next;
 	stats_dumparg_t dumparg;
 	uint64_t nsstat_values[ns_statscounter_max];
 	uint64_t resstat_values[dns_resstatscounter_max];
 	uint64_t adbstat_values[dns_adbstats_max];
 	uint64_t zonestat_values[dns_zonestatscounter_max];
 	uint64_t sockstat_values[isc_sockstatscounter_max];
-	uint64_t gluecachestats_values[dns_gluecachestatscounter_max];
 	isc_stdtime_t now = isc_stdtime_now();

 	isc_once_do(&once, init_desc);
@@ -4291,75 +4340,12 @@ named_stats_dump(named_server_t *server, FILE *fp) {
 			 sockstats_index, sockstat_values, 0);

 	fprintf(fp, "++ Per Zone Query Statistics ++\n");
-	zone = NULL;
-	for (result = dns_zonemgr_first_zone(server->zonemgr, &zone);
-	     result == ISC_R_SUCCESS; next = NULL,
-	    result = dns_zonemgr_next_zone(zone, &next), zone = next)
-	{
-		if (dns_zone_getstatlevel(zone) != dns_zonestat_full) {
-			continue;
-		}
-
-		isc_stats_t *zonestats = dns_zone_getrequeststats(zone);
-
-		if (zonestats != NULL) {
-			char zonename[DNS_NAME_FORMATSIZE];
-			dns_view_t *view = dns_zone_getview(zone);
-			if (view == NULL) {
-				continue;
-			}
-
-			dns_name_format(dns_zone_getorigin(zone), zonename,
-					sizeof(zonename));
-			fprintf(fp, "[%s", zonename);
-			if (strcmp(view->name, "_default") != 0) {
-				fprintf(fp, " (view: %s)", view->name);
-			}
-			fprintf(fp, "]\n");
-
-			(void)dump_stats(zonestats, isc_statsformat_file, fp,
-					 NULL, nsstats_desc,
-					 ns_statscounter_max, nsstats_index,
-					 nsstat_values, 0);
-		}
-	}
+	RETERR(named_server_applyzones(server, dump_zone_query_stats, fp));

 	fprintf(fp, "++ Per Zone Glue Cache Statistics ++\n");
-	zone = NULL;
-	for (result = dns_zonemgr_first_zone(server->zonemgr, &zone);
-	     result == ISC_R_SUCCESS; next = NULL,
-	    result = dns_zonemgr_next_zone(zone, &next), zone = next)
-	{
-		if (dns_zone_getstatlevel(zone) != dns_zonestat_full) {
-			continue;
-		}
-
-		isc_stats_t *gluecachestats = dns_zone_getgluecachestats(zone);
-
-		if (gluecachestats != NULL) {
-			char zonename[DNS_NAME_FORMATSIZE];
-			dns_view_t *view = dns_zone_getview(zone);
-			if (view == NULL) {
-				continue;
-			}
-
-			dns_name_format(dns_zone_getorigin(zone), zonename,
-					sizeof(zonename));
-			fprintf(fp, "[%s", zonename);
-			if (strcmp(view->name, "_default") != 0) {
-				fprintf(fp, " (view: %s)", view->name);
-			}
-			fprintf(fp, "]\n");
-
-			(void)dump_stats(gluecachestats, isc_statsformat_file,
-					 fp, NULL, gluecachestats_desc,
-					 dns_gluecachestatscounter_max,
-					 gluecachestats_index,
-					 gluecachestats_values, 0);
-		}
-	}
+	RETERR(named_server_applyzones(server, dump_zone_glue_stats, fp));

 	fprintf(fp, "--- Statistics Dump --- (%lu)\n", (unsigned long)now);

-	return ISC_R_SUCCESS; /* this function currently always succeeds */
+	return ISC_R_SUCCESS;
 }
diff --git a/bin/tests/system/dyndb/driver/instance.c b/bin/tests/system/dyndb/driver/instance.c
index cf4d075af1..85180b9a86 100644
--- a/bin/tests/system/dyndb/driver/instance.c
+++ b/bin/tests/system/dyndb/driver/instance.c
@@ -133,7 +133,7 @@ new_sample_instance(isc_mem_t *mctx, const char *db_name, int argc, char **argv,
 	}

 	dns_view_attach(dctx->view, &inst->view);
-	dns_zonemgr_attach(dctx->zmgr, &inst->zmgr);
+	inst->zmgr = dctx->zmgr;

 	/* Register new DNS DB implementation. */
 	result = dns_db_register(db_name, create_db, inst, mctx, &inst->db_imp);
@@ -221,7 +221,6 @@ destroy_sample_instance(sample_instance_t **instp) {
 	}

 	dns_view_detach(&inst->view);
-	dns_zonemgr_detach(&inst->zmgr);

 	isc_mem_putanddetach(&inst->mctx, inst, sizeof(*inst));
 }
diff --git a/bin/tests/system/dyndb/driver/zone.c b/bin/tests/system/dyndb/driver/zone.c
index d366a8ef67..9bbdff9e48 100644
--- a/bin/tests/system/dyndb/driver/zone.c
+++ b/bin/tests/system/dyndb/driver/zone.c
@@ -68,21 +68,12 @@ create_zone(sample_instance_t *const inst, dns_name_t *const name,

 	zone_argv[0] = inst->db_name;

-	dns_zone_create(&raw, inst->mctx, 0); /* FIXME: all zones are assigned
-						 to loop 0 */
+	CHECK(dns_zonemgr_createzone(inst->zmgr, &raw));
 	dns_zone_setorigin(raw, name);
 	dns_zone_setclass(raw, dns_rdataclass_in);
 	dns_zone_settype(raw, dns_zone_primary);
 	dns_zone_setdbtype(raw, 1, zone_argv);

-	result = dns_zonemgr_managezone(inst->zmgr, raw);
-	if (result != ISC_R_SUCCESS) {
-		log_write(ISC_LOG_ERROR,
-			  "create_zone: dns_zonemgr_managezone -> %s\n",
-			  isc_result_totext(result));
-		goto cleanup;
-	}
-
 	/* This is completely insecure - use some sensible values instead! */
 	dns_acl_any(inst->mctx, &acl_any);
 	dns_zone_setupdateacl(raw, acl_any);
@@ -98,9 +89,6 @@ cleanup:
 	log_error_r("failed to create new zone '%s'", zone_name);

 	if (raw != NULL) {
-		if (dns_zone_getmgr(raw) != NULL) {
-			dns_zonemgr_releasezone(inst->zmgr, raw);
-		}
 		dns_zone_detach(&raw);
 	}

diff --git a/bin/tests/system/shutdown/forwarder/forward.db.j2 b/bin/tests/system/shutdown/forwarder/forward.db.j2
new file mode 100644
index 0000000000..fcc1366784
--- /dev/null
+++ b/bin/tests/system/shutdown/forwarder/forward.db.j2
@@ -0,0 +1,4 @@
+$TTL 300
+forward.test. IN SOA ns.forward.test. hostmaster.forward.test. 1 3600 3600 86400 300
+  IN NS ns.forward.test.
+ns IN A 10.53.0.4
diff --git a/bin/tests/system/shutdown/forwarder/named.conf.j2 b/bin/tests/system/shutdown/forwarder/named.conf.j2
new file mode 100644
index 0000000000..20a1b4faa3
--- /dev/null
+++ b/bin/tests/system/shutdown/forwarder/named.conf.j2
@@ -0,0 +1,26 @@
+key rndc_key {
+	secret "1234abcd8765";
+	algorithm @DEFAULT_HMAC@;
+};
+controls {
+	inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+options {
+	port @PORT@;
+	listen-on { 10.53.0.3; };
+	listen-on-v6 { none; };
+	pid-file "named.pid";
+	recursion no;
+	dnssec-validation no;
+	notify no;
+};
+view "@ 'replacement' if retired | default(false) else 'original' @" {
+{% if not retired | default(false) %}
+	zone "forward.test" {
+		type secondary;
+		file "forward.db";
+		primaries { 10.53.0.4 port @PORT@; };
+		allow-update-forwarding { any; };
+	};
+{% endif %}
+};
diff --git a/bin/tests/system/shutdown/tests_shutdown.py b/bin/tests/system/shutdown/tests_shutdown.py
index 9e44c1fd7c..c5bff8e04e 100644
--- a/bin/tests/system/shutdown/tests_shutdown.py
+++ b/bin/tests/system/shutdown/tests_shutdown.py
@@ -15,10 +15,15 @@ from string import ascii_lowercase as letters
 import os
 import random
 import signal
+import socket
+import struct
 import subprocess
 import time

 import dns.exception
+import dns.message
+import dns.opcode
+import dns.update
 import pytest

 import isctest
@@ -27,6 +32,10 @@ pytestmark = pytest.mark.extra_artifacts(
     [
         "resolver/named.conf",
         "resolver/named.run",
+        "forwarder/named.conf",
+        "forwarder/named.run",
+        "forwarder/forward.db",
+        "forwarder/db-*",
     ]
 )

@@ -185,3 +194,75 @@ def test_named_shutdown(kill_method):
                 assert named_proc.returncode == 0, "named crashed"
             finally:  # Ensure named is terminated in case of an exception
                 named_proc.kill()
+
+
+@pytest.mark.parametrize("retired_view", [False, True])
+@pytest.mark.parametrize("kill_method", ["rndc", "sigterm"])
+def test_shutdown_pending_forward(retired_view, kill_method, templates, named_port):
+    """
+    Shutdown must not wait for an unanswered forwarded UPDATE.
+
+    Keep the primary connection open until named exits.  In particular, do not
+    let closing the test socket accidentally unblock shutdown.  Reconfiguring
+    with a different view name leaves the old view held by the pending client.
+    """
+    cfg_dir = "forwarder"
+    templates.render(f"{cfg_dir}/named.conf", {"retired": False})
+    templates.render(f"{cfg_dir}/forward.db")
+    instance = isctest.instance.NamedInstance(cfg_dir, num=3)
+
+    def receive_exact(connection, length):
+        result = b""
+        while len(result) < length:
+            chunk = connection.recv(length - len(result))
+            assert chunk, "forwarding connection closed before UPDATE arrived"
+            result += chunk
+        return result
+
+    with socket.socket() as primary, socket.socket(
+        socket.AF_INET, socket.SOCK_DGRAM
+    ) as client:
+        primary.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        primary.bind(("10.53.0.4", named_port))
+        primary.listen()
+        primary.settimeout(10)
+        client.settimeout(0.2)
+        with open(f"{cfg_dir}/named.run", "ab") as log:
+            with subprocess.Popen(
+                isctest.run.get_named_cmdline(cfg_dir), cwd=cfg_dir, stderr=log
+            ) as proc:
+                try:
+                    isctest.check.named_alive(proc, "10.53.0.3")
+                    update = dns.update.Update("forward.test")
+                    update.add("pending", 300, "A", "192.0.2.1")
+                    client.sendto(update.to_wire(), ("10.53.0.3", named_port))
+                    connection, _ = primary.accept()
+                    with connection:
+                        connection.settimeout(10)
+                        length = struct.unpack("!H", receive_exact(connection, 2))[0]
+                        forwarded = dns.message.from_wire(
+                            receive_exact(connection, length)
+                        )
+                        assert forwarded.opcode() == dns.opcode.UPDATE
+                        assert forwarded.question == update.question
+                        if retired_view:
+                            templates.render(f"{cfg_dir}/named.conf", {"retired": True})
+                            instance.rndc("reconfig")
+                            # Prove the old view is no longer in the active config.
+                            result = instance.rndc(
+                                "showzone forward.test IN original",
+                                raise_on_exception=False,
+                            )
+                            assert result.rc != 0
+                        # The operation must still be outstanding at shutdown.
+                        with pytest.raises(socket.timeout):
+                            client.recv(65535)
+                        if kill_method == "rndc":
+                            instance.rndc("stop")
+                        else:
+                            proc.terminate()
+                        assert proc.wait(timeout=10) == 0
+                finally:
+                    if proc.poll() is None:
+                        proc.kill()
+                        proc.wait()
diff --git a/lib/dns/dlz.c b/lib/dns/dlz.c
index 5adc697794..7331b5a294 100644
--- a/lib/dns/dlz.c
+++ b/lib/dns/dlz.c
@@ -71,6 +71,7 @@
 #include <dns/master.h>
 #include <dns/ssu.h>
 #include <dns/zone.h>
+#include <dns/zonemgr.h>
 #include <dns/zoneproperties.h>

 #include "dlz_p.h"
@@ -423,7 +424,7 @@ dns_dlz_writeablezone(dns_view_t *view, dns_dlzdb_t *dlzdb,
 	INSIST(dupzone == NULL);

 	/* Create it */
-	dns_zone_create(&zone, view->mctx, 0);
+	CHECK(dns_zonemgr_createzone(dns_g_zonemgr, &zone));
 	dns_zone_setorigin(zone, origin);
 	dns_zone_setview(zone, view);

diff --git a/lib/dns/dyndb.c b/lib/dns/dyndb.c
index 55043ec2ee..b7feeba33b 100644
--- a/lib/dns/dyndb.c
+++ b/lib/dns/dyndb.c
@@ -265,9 +265,8 @@ dns_dyndb_createctx(isc_mem_t *mctx, const void *hashinit, dns_view_t *view,
 	if (view != NULL) {
 		dns_view_attach(view, &dctx->view);
 	}
-	if (zmgr != NULL) {
-		dns_zonemgr_attach(zmgr, &dctx->zmgr);
-	}
+	/* The runtime keeps the manager alive through module cleanup. */
+	dctx->zmgr = zmgr;

 	isc_mem_attach(mctx, &dctx->mctx);
 	dctx->magic = DNS_DYNDBCTX_MAGIC;
@@ -289,9 +288,6 @@ dns_dyndb_destroyctx(dns_dyndbctx_t **dctxp) {
 	if (dctx->view != NULL) {
 		dns_view_detach(&dctx->view);
 	}
-	if (dctx->zmgr != NULL) {
-		dns_zonemgr_detach(&dctx->zmgr);
-	}

 	isc_mem_putanddetach(&dctx->mctx, dctx, sizeof(*dctx));
 }
diff --git a/lib/dns/include/dns/dyndb.h b/lib/dns/include/dns/dyndb.h
index 4f6b376c3e..64919af80a 100644
--- a/lib/dns/include/dns/dyndb.h
+++ b/lib/dns/include/dns/dyndb.h
@@ -28,7 +28,8 @@
  * initializer, log context, etc.  The structure doesn't persist
  * beyond configuring the dyndb module. The module's register function
  * should attach to all reference-counted variables and its destroy
- * function should detach from them.
+ * function should detach from them. The zone manager is borrowed: the runtime
+ * keeps it alive until all modules and their outstanding zone activity finish.
  */
 struct dns_dyndbctx {
 	unsigned int   magic;
@@ -51,7 +52,7 @@ struct dns_dyndbctx {
  * if not, set DNS_DYNDB_AGE to 0.
  */
 #ifndef DNS_DYNDB_VERSION
-#define DNS_DYNDB_VERSION 3
+#define DNS_DYNDB_VERSION 4
 #define DNS_DYNDB_AGE	  0
 #endif /* ifndef DNS_DYNDB_VERSION */

diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h
index 633315add0..b06b40f783 100644
--- a/lib/dns/include/dns/zone.h
+++ b/lib/dns/include/dns/zone.h
@@ -355,6 +355,15 @@ dns_zone_markdirty(dns_zone_t *zone);
  *\li	'zone' to be a valid zone.
  */

+void
+dns_zone_forcemaint(dns_zone_t *zone);
+/*%<
+ * Reconsider the maintenance timer of a configured zone.
+ *
+ * Requires:
+ *\li	'zone' to be a valid zone.
+ */
+
 void
 dns_zone_refresh(dns_zone_t *zone);
 /*%<
@@ -596,6 +605,17 @@ dns_zone_prepare_shutdown(dns_zone_t *zone);
  *\li	'zone' to be a valid initialised zone.
  */

+unsigned int
+dns_zone_getcount(dns_zone_t *zone, dns_zonestate_t state);
+/*%<
+ * Return this zone's contribution to a non-transfer status count.
+ *
+ * Requires:
+ *\li	'zone' to be a valid zone.
+ *\li	'state' is DNS_ZONESTATE_XFERFIRSTREFRESH, DNS_ZONESTATE_SOAQUERY,
+ *	DNS_ZONESTATE_ANY, or DNS_ZONESTATE_AUTOMATIC.
+ */
+
 isc_result_t
 dns_zone_getxfr(dns_zone_t *zone, dns_xfrin_t **xfrp, bool *is_firstrefresh,
 		bool *is_running, bool *is_deferred, bool *is_presoa,
diff --git a/lib/dns/include/dns/zonemgr.h b/lib/dns/include/dns/zonemgr.h
index e1478826aa..00426aad71 100644
--- a/lib/dns/include/dns/zonemgr.h
+++ b/lib/dns/include/dns/zonemgr.h
@@ -17,10 +17,17 @@

 #include <dns/zone.h>

+/*
+ * Process-wide shared zone services owned by the runtime. Borrowed by zones
+ * and DynDB modules; valid until the runtime calls dns_zonemgr_destroy().
+ */
+extern dns_zonemgr_t *dns_g_zonemgr;
+
 void
 dns_zonemgr_create(isc_mem_t *mctx, dns_zonemgr_t **zmgrp);
 /*%<
- * Create a zone manager.
+ * Create the process-wide zone manager. Only one may exist at a time.
+ * The caller owns its lifetime and must destroy it after all loops drain.
  *
  * Requires:
  *\li	'mctx' to be a valid memory context.
@@ -31,75 +38,43 @@ isc_result_t
 dns_zonemgr_createzone(dns_zonemgr_t *zmgr, dns_zone_t **zonep);
 /*%<
  *	Allocate a new zone using a memory context from the
- *	zone manager's memory context pool.
+ *	zone manager's memory context pool, attached to an existing event loop.
+ *	The runtime must keep the manager alive until zone cleanup finishes.
  *
  * Require:
  *\li	'zmgr' to be a valid zone manager.
  *\li	'zonep' != NULL and '*zonep' == NULL.
  */

-isc_result_t
-dns_zonemgr_managezone(dns_zonemgr_t *zmgr, dns_zone_t *zone);
-/*%<
- *	Bring the zone under control of a zone manager.
- *
- * Require:
- *\li	'zmgr' to be a valid zone manager.
- *\li	'zone' to be a valid zone.
- */
-
-isc_result_t
-dns_zonemgr_forcemaint(dns_zonemgr_t *zmgr);
-/*%<
- * Force zone maintenance of all loaded zones managed by 'zmgr'
- * to take place at the system's earliest convenience.
- */
-
 void
-dns_zonemgr_shutdown(dns_zonemgr_t *zmgr);
+dns_zonemgr_resumexfrs(dns_zonemgr_t *zmgr);
 /*%<
- *	Shut down the zone manager.
+ * Retry transfers waiting for quota after a configuration change.
+ * Zone timer maintenance is scheduled separately by the view owner.
  *
  * Requires:
  *\li	'zmgr' to be a valid zone manager.
  */

 void
-dns_zonemgr_attach(dns_zonemgr_t *source, dns_zonemgr_t **target);
-/*%<
- *	Attach '*target' to 'source' incrementing its external
- * 	reference count.
- *
- * Require:
- *\li	'zone' to be a valid zone.
- *\li	'target' to be non NULL and '*target' to be NULL.
- */
-
-void
-dns_zonemgr_detach(dns_zonemgr_t **zmgrp);
+dns_zonemgr_shutdown(dns_zonemgr_t *zmgr);
 /*%<
- *	 Detach from a zone manager.
+ *	Stop zone services while the event loops are running. The manager
+ * remains allocated until dns_zonemgr_destroy() is called after the loops
+ * drain.
  *
  * Requires:
- *\li	'*zmgrp' is a valid, non-NULL zone manager pointer.
- *
- * Ensures:
- *\li	'*zmgrp' is NULL.
+ *\li	'zmgr' to be a valid zone manager.
  */

 void
-dns_zonemgr_releasezone(dns_zonemgr_t *zmgr, dns_zone_t *zone);
+dns_zonemgr_destroy(dns_zonemgr_t **zmgrp);
 /*%<
- *	Release 'zone' from the managed by 'zmgr'.  'zmgr' is implicitly
- *	detached from 'zone'.
- *
- * Requires:
- *\li	'zmgr' to be a valid zone manager.
- *\li	'zone' to be a valid zone.
- *\li	'zmgr' == 'zone->zmgr'
+ * Destroy the process-wide zone manager and clear '*zmgrp' and dns_g_zonemgr.
  *
- * Ensures:
- *\li	'zone->zmgr' == NULL;
+ * The runtime must first shut down the manager and all DynDB instances, then
+ * drain all zone activity by waiting for isc_loopmgr_run() to return. Zones
+ * and DynDB instances borrow the manager and must not outlive it.
  */

 void
@@ -209,7 +184,7 @@ dns_zonemgr_getcount(dns_zonemgr_t *zmgr, dns_zonestate_t state);
  *
  * Requires:
  *\li	'zmgr' to be a valid zone manager.
- *\li	'state' to be a valid DNS_ZONESTATE_ enum.
+ *\li	'state' is DNS_ZONESTATE_XFERRUNNING or DNS_ZONESTATE_XFERDEFERRED.
  */

 void
@@ -223,35 +198,3 @@ dns_zonemgr_set_tlsctx_cache(dns_zonemgr_t	*zmgr,
  *\li	'zmgr' is a valid zone manager.
  *\li	'tlsctx_cache' is a valid TLS context cache.
  */
-
-isc_result_t
-dns_zonemgr_next_zone(dns_zone_t *zone, dns_zone_t **next);
-/*%<
- * Find the next zone in the list of managed zones.
- *
- * Requires:
- *\li	'zone' to be valid
- *\li	The zone manager for the indicated zone MUST be locked
- *	by the caller.  This is not checked.
- *\li	'next' be non-NULL, and '*next' be NULL.
- *
- * Ensures:
- *\li	'next' points to a valid zone (result ISC_R_SUCCESS) or to NULL
- *	(result ISC_R_NOMORE).
- */
-
-isc_result_t
-dns_zonemgr_first_zone(dns_zonemgr_t *zmgr, dns_zone_t **first);
-/*%<
- * Find the first zone in the list of managed zones.
- *
- * Requires:
- *\li	'zonemgr' to be valid
- *\li	The zone manager for the indicated zone MUST be locked
- *	by the caller.  This is not checked.
- *\li	'first' be non-NULL, and '*first' be NULL
- *
- * Ensures:
- *\li	'first' points to a valid zone (result ISC_R_SUCCESS) or to NULL
- *	(result ISC_R_NOMORE).
- */
diff --git a/lib/dns/include/dns/zoneproperties.h b/lib/dns/include/dns/zoneproperties.h
index c9c4c3ff17..41631b5527 100644
--- a/lib/dns/include/dns/zoneproperties.h
+++ b/lib/dns/include/dns/zoneproperties.h
@@ -918,15 +918,6 @@ dns_zone_getmctx(dns_zone_t *zone);
  * \li	'zone' to be a valid zone.
  */

-dns_zonemgr_t *
-dns_zone_getmgr(dns_zone_t *zone);
-/*%<
- *	If 'zone' is managed return the zone manager otherwise NULL.
- *
- * Requires:
- * \li	'zone' to be a valid zone.
- */
-
 void
 dns_zone_setsigvalidityinterval(dns_zone_t *zone, uint32_t interval);
 /*%<
diff --git a/lib/dns/notify.c b/lib/dns/notify.c
index 979ee35c46..73bedf2ccc 100644
--- a/lib/dns/notify.c
+++ b/lib/dns/notify.c
@@ -26,6 +26,7 @@
 #include <dns/stats.h>
 #include <dns/tsig.h>
 #include <dns/zone.h>
+#include <dns/zonemgr.h>
 #include <dns/zoneproperties.h>

 #include "zone_p.h"
@@ -339,7 +340,7 @@ notify_send_toaddr(void *arg) {
 	dns__zone_lock(notify->zone);

 	notifyctx = dns__zone_getnotifyctx(notify->zone, notify->type);
-	zmgr = dns_zone_getmgr(notify->zone);
+	zmgr = dns_g_zonemgr;
 	view = dns_zone_getview(notify->zone);
 	loop = dns_zone_getloop(notify->zone);
 	result = dns_zone_getdb(notify->zone, &zonedb);
@@ -526,7 +527,7 @@ notify_queue(dns_notify_t *notify, bool startup, bool dequeue) {
 	REQUIRE(DNS_NOTIFY_VALID(notify));

 	isc_loop_t *loop = dns_zone_getloop(notify->zone);
-	dns_zonemgr_t *zmgr = dns_zone_getmgr(notify->zone);
+	dns_zonemgr_t *zmgr = dns_g_zonemgr;
 	isc_ratelimiter_t *notifyrl = NULL;
 	isc_ratelimiter_t *startupnotifyrl = NULL;

diff --git a/lib/dns/xfrin.c b/lib/dns/xfrin.c
index 9bcdf5fc43..06755fffb0 100644
--- a/lib/dns/xfrin.c
+++ b/lib/dns/xfrin.c
@@ -1434,7 +1434,6 @@ xfrin_connect_done(isc_result_t eresult, isc_region_t *region ISC_ATTR_UNUSED,
 	char addrtext[ISC_SOCKADDR_FORMATSIZE];
 	char signerbuf[DNS_NAME_FORMATSIZE];
 	const char *signer = "", *sep = "";
-	dns_zonemgr_t *zmgr = NULL;
 	isc_result_t result;

 	REQUIRE(VALID_XFRIN(xfr));
@@ -1454,8 +1453,7 @@ xfrin_connect_done(isc_result_t eresult, isc_region_t *region ISC_ATTR_UNUSED,
 		goto cleanup;
 	}

-	zmgr = dns_zone_getmgr(xfr->zone);
-	if (zmgr != NULL) {
+	if (dns_zone_getloop(xfr->zone) != NULL) {
 		dns_view_t *view = dns_zone_getview(xfr->zone);
 		dns_unreachcache_remove(view->unreachcache, &xfr->primaryaddr,
 					&xfr->sourceaddr);
@@ -1493,8 +1491,7 @@ cleanup:
 		 * the server has a permanent networking error or
 		 * the connection attempt as timed out.
 		 */
-		zmgr = dns_zone_getmgr(xfr->zone);
-		if (zmgr != NULL) {
+		if (dns_zone_getloop(xfr->zone) != NULL) {
 			dns_view_t *view = dns_zone_getview(xfr->zone);
 			dns_unreachcache_add(view->unreachcache,
 					     &xfr->primaryaddr,
diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index 15ffc0241c..03dc3eeb26 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -512,7 +512,6 @@ dns_zone_create(dns_zone_t **zonep, isc_mem_t *mctx, isc_tid_t tid) {
 		.nsec3chain = ISC_LIST_INITIALIZER,
 		.maintenance_queue = ISC_LIST_INITIALIZER,
 		.forwards = ISC_LIST_INITIALIZER,
-		.link = ISC_LINK_INITIALIZER,
 		.statelink = ISC_LINK_INITIALIZER,
 	};
 	dns_remote_t r = {
@@ -561,7 +560,6 @@ dns__zone_free(dns_zone_t *zone) {
 	REQUIRE(DNS_ZONE_VALID(zone));
 	REQUIRE(!LOCKED_ZONE(zone));
 	REQUIRE(zone->timer == NULL);
-	REQUIRE(zone->zmgr == NULL);

 	dns_zone_unloadplugins(zone);

@@ -711,6 +709,9 @@ dns__zone_free(dns_zone_t *zone) {
 	}

 	/* last stuff */
+	if (zone->loop != NULL) {
+		isc_loop_detach(&zone->loop);
+	}
 	ZONEDB_DESTROYLOCK(&zone->dblock);
 	isc_mutex_destroy(&zone->lock);
 	zone->magic = 0;
@@ -1487,7 +1488,7 @@ dns_zone_asyncload(dns_zone_t *zone, bool newonly, dns_zt_callback_t *done,

 	REQUIRE(DNS_ZONE_VALID(zone));

-	if (zone->zmgr == NULL) {
+	if (zone->loop == NULL) {
 		return ISC_R_FAILURE;
 	}

@@ -1714,7 +1715,7 @@ zone_startload(dns_db_t *db, dns_zone_t *zone, isc_time_t loadtime) {

 	CHECK(dns_db_beginload(db, &load->callbacks));

-	if (zone->zmgr != NULL && zone->db != NULL) {
+	if (zone->loop != NULL && zone->db != NULL) {
 		CHECK(dns_master_loadfileasync(
 			zone->masterfile, dns_db_origin(db), dns_db_origin(db),
 			zone->rdclass, options, 0, &load->callbacks, zone->loop,
@@ -4583,6 +4584,41 @@ dns__zone_free_check(dns_zone_t *zone) {
 	return false;
 }

+void
+dns_zone_forcemaint(dns_zone_t *zone) {
+	REQUIRE(DNS_ZONE_VALID(zone));
+	LOCK_ZONE(zone);
+	dns__zone_settimer(zone, isc_time_now());
+	UNLOCK_ZONE(zone);
+}
+
+unsigned int
+dns_zone_getcount(dns_zone_t *zone, dns_zonestate_t state) {
+	unsigned int count = 0;
+	REQUIRE(DNS_ZONE_VALID(zone));
+	LOCK_ZONE(zone);
+	switch (state) {
+	case DNS_ZONESTATE_XFERFIRSTREFRESH:
+		count = DNS_ZONE_FLAG(zone, DNS_ZONEFLG_FIRSTREFRESH);
+		break;
+	case DNS_ZONESTATE_SOAQUERY:
+		count = DNS_ZONE_FLAG(zone, DNS_ZONEFLG_REFRESH);
+		break;
+	case DNS_ZONESTATE_ANY:
+	case DNS_ZONESTATE_AUTOMATIC:
+		if (zone->view == NULL ||
+		    strcmp(zone->view->name, "_bind") != 0)
+		{
+			count = state == DNS_ZONESTATE_ANY || zone->automatic;
+		}
+		break;
+	default:
+		UNREACHABLE();
+	}
+	UNLOCK_ZONE(zone);
+	return count;
+}
+
 static bool
 zone_check_ns(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *version,
 	      dns_name_t *name, bool logit) {
@@ -12534,7 +12570,7 @@ queue_soa_query(dns_zone_t *zone) {
 	 * Attach so that we won't clean up until the event is delivered.
 	 */
 	zone_iattach(zone, &sq->zone);
-	result = isc_ratelimiter_enqueue(zone->zmgr->refreshrl, zone->loop,
+	result = isc_ratelimiter_enqueue(dns_g_zonemgr->refreshrl, zone->loop,
 					 soa_query, sq, &sq->rlevent);
 	if (result != ISC_R_SUCCESS) {
 		zone_idetach(&sq->zone);
@@ -13061,24 +13097,23 @@ zone_shutdown(void *arg) {
 	/*
 	 * If we were waiting for xfrin quota, step out of
 	 * the queue.
-	 * If there's no zone manager, we can't be waiting for the
-	 * xfrin quota
+	 * A zone without an event loop cannot be waiting for transfer quota.
 	 */
-	if (zone->zmgr != NULL) {
-		RWLOCK(&zone->zmgr->rwlock, isc_rwlocktype_write);
-		if (zone->statelist == &zone->zmgr->waiting_for_xfrin) {
-			ISC_LIST_UNLINK(zone->zmgr->waiting_for_xfrin, zone,
+	if (zone->loop != NULL) {
+		RWLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_write);
+		if (zone->statelist == &dns_g_zonemgr->waiting_for_xfrin) {
+			ISC_LIST_UNLINK(dns_g_zonemgr->waiting_for_xfrin, zone,
 					statelink);
 			linked = true;
 			zone->statelist = NULL;
 		}
-		if (zone->statelist == &zone->zmgr->xfrin_in_progress) {
-			ISC_LIST_UNLINK(zone->zmgr->xfrin_in_progress, zone,
+		if (zone->statelist == &dns_g_zonemgr->xfrin_in_progress) {
+			ISC_LIST_UNLINK(dns_g_zonemgr->xfrin_in_progress, zone,
 					statelink);
 			zone->statelist = NULL;
-			dns__zonemgr_resume_xfrs(zone->zmgr, false);
+			dns__zonemgr_resume_xfrs(dns_g_zonemgr, false);
 		}
-		RWUNLOCK(&zone->zmgr->rwlock, isc_rwlocktype_write);
+		RWUNLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_write);
 	}

 	/*
@@ -13089,11 +13124,6 @@ zone_shutdown(void *arg) {
 		dns_xfrin_shutdown(zone->xfr);
 	}

-	/* Safe to release the zone now */
-	if (zone->zmgr != NULL) {
-		dns_zonemgr_releasezone(zone->zmgr, zone);
-	}
-
 	/* Detach the zone configuration pointer */
 	dns_zone_setcfg(zone, NULL, NULL);

@@ -15712,15 +15742,16 @@ again:
 	 * This transfer finishing freed up a transfer quota slot.
 	 * Let any other zones waiting for quota have it.
 	 */
-	if (zone->zmgr != NULL &&
-	    zone->statelist == &zone->zmgr->xfrin_in_progress)
+	if (zone->loop != NULL &&
+	    zone->statelist == &dns_g_zonemgr->xfrin_in_progress)
 	{
 		UNLOCK_ZONE(zone);
-		RWLOCK(&zone->zmgr->rwlock, isc_rwlocktype_write);
-		ISC_LIST_UNLINK(zone->zmgr->xfrin_in_progress, zone, statelink);
+		RWLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_write);
+		ISC_LIST_UNLINK(dns_g_zonemgr->xfrin_in_progress, zone,
+				statelink);
 		zone->statelist = NULL;
-		dns__zonemgr_resume_xfrs(zone->zmgr, false);
-		RWUNLOCK(&zone->zmgr->rwlock, isc_rwlocktype_write);
+		dns__zonemgr_resume_xfrs(dns_g_zonemgr, false);
+		RWUNLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_write);
 		LOCK_ZONE(zone);
 	}

@@ -15814,7 +15845,7 @@ again:
 static void
 queue_xfrin(dns_zone_t *zone) {
 	isc_result_t result;
-	dns_zonemgr_t *zmgr = zone->zmgr;
+	dns_zonemgr_t *zmgr = dns_g_zonemgr;

 	ENTER;

@@ -15935,7 +15966,7 @@ next:
 		}
 	}

-	dns__zonemgr_tlsctx_attach(zone->zmgr, &zmgr_tlsctx_cache);
+	dns__zonemgr_tlsctx_attach(dns_g_zonemgr, &zmgr_tlsctx_cache);
 	const unsigned int connect_timeout = isc_nm_getprimariestimeout() /
 					     MS_PER_SEC;
 	result = dns_request_createraw(
@@ -16182,15 +16213,15 @@ dns_zone_stopxfr(dns_zone_t *zone) {

 	REQUIRE(DNS_ZONE_VALID(zone));

-	RWLOCK(&zone->zmgr->rwlock, isc_rwlocktype_read);
+	RWLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_read);
 	LOCK_ZONE(zone);
-	if (zone->statelist == &zone->zmgr->xfrin_in_progress &&
+	if (zone->statelist == &dns_g_zonemgr->xfrin_in_progress &&
 	    zone->xfr != NULL)
 	{
 		dns_xfrin_attach(zone->xfr, &xfr);
 	}
 	UNLOCK_ZONE(zone);
-	RWUNLOCK(&zone->zmgr->rwlock, isc_rwlocktype_read);
+	RWUNLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_read);

 	if (xfr != NULL) {
 		dns_xfrin_shutdown(xfr);
@@ -16278,7 +16309,7 @@ dns_zone_getxfr(dns_zone_t *zone, dns_xfrin_t **xfrp, bool *is_firstrefresh,
 	REQUIRE(DNS_ZONE_VALID(zone));
 	REQUIRE(xfrp != NULL && *xfrp == NULL);

-	if (zone->zmgr == NULL) {
+	if (zone->loop == NULL) {
 		return ISC_R_FAILURE;
 	}

@@ -16290,20 +16321,20 @@ dns_zone_getxfr(dns_zone_t *zone, dns_xfrin_t **xfrp, bool *is_firstrefresh,
 	*is_pending = false;
 	*needs_refresh = false;

-	RWLOCK(&zone->zmgr->rwlock, isc_rwlocktype_read);
+	RWLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_read);
 	LOCK_ZONE(zone);
 	*is_firstrefresh = DNS_ZONE_FLAG(zone, DNS_ZONEFLG_FIRSTREFRESH);
 	if (zone->xfr != NULL) {
 		dns_xfrin_attach(zone->xfr, xfrp);
 	}
-	if (zone->statelist == &zone->zmgr->xfrin_in_progress) {
+	if (zone->statelist == &dns_g_zonemgr->xfrin_in_progress) {
 		*is_running = true;
 		/*
 		 * The NEEDREFRESH flag is set only when a notify was received
 		 * while the current zone transfer is running.
 		 */
 		*needs_refresh = DNS_ZONE_FLAG(zone, DNS_ZONEFLG_NEEDREFRESH);
-	} else if (zone->statelist == &zone->zmgr->waiting_for_xfrin) {
+	} else if (zone->statelist == &dns_g_zonemgr->waiting_for_xfrin) {
 		*is_deferred = true;
 	} else if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_REFRESH)) {
 		if (zone->request != NULL) {
@@ -16329,7 +16360,7 @@ dns_zone_getxfr(dns_zone_t *zone, dns_xfrin_t **xfrp, bool *is_firstrefresh,
 		}
 	}
 	UNLOCK_ZONE(zone);
-	RWUNLOCK(&zone->zmgr->rwlock, isc_rwlocktype_read);
+	RWUNLOCK(&dns_g_zonemgr->rwlock, isc_rwlocktype_read);

 	return ISC_R_SUCCESS;
 }
@@ -17570,7 +17601,7 @@ checkds_send_toaddr(void *arg) {

 	options |= DNS_REQUESTOPT_TCP;

-	dns__zonemgr_tlsctx_attach(checkds->zone->zmgr, &zmgr_tlsctx_cache);
+	dns__zonemgr_tlsctx_attach(dns_g_zonemgr, &zmgr_tlsctx_cache);
 	const unsigned int connect_timeout = isc_nm_getinitialtimeout() /
 					     MS_PER_SEC;

@@ -17644,10 +17675,9 @@ checkds_send_tons(dns_checkds_t *checkds) {
 			UNREACHABLE();
 		}

-		CHECK(isc_ratelimiter_enqueue(newcheckds->zone->zmgr->checkdsrl,
-					      newcheckds->zone->loop,
-					      checkds_send_toaddr, newcheckds,
-					      &newcheckds->rlevent));
+		CHECK(isc_ratelimiter_enqueue(
+			dns_g_zonemgr->checkdsrl, newcheckds->zone->loop,
+			checkds_send_toaddr, newcheckds, &newcheckds->rlevent));
 		newcheckds = NULL;
 	}

@@ -17764,7 +17794,7 @@ checkds_send(dns_zone_t *zone) {

 		ISC_LIST_APPEND(zone->checkds_requests, checkds, link);
 		result = isc_ratelimiter_enqueue(
-			checkds->zone->zmgr->checkdsrl, checkds->zone->loop,
+			dns_g_zonemgr->checkdsrl, checkds->zone->loop,
 			checkds_send_toaddr, checkds, &checkds->rlevent);
 		if (result != ISC_R_SUCCESS) {
 			dns_zone_log(zone, ISC_LOG_DEBUG(3),
@@ -19715,32 +19745,30 @@ again:
 }

 /*
- * Lock hierarchy: zmgr, zone, raw.
+ * Link a fresh raw zone to its signed counterpart on the same event loop.
  */
 isc_result_t
 dns_zone_link(dns_zone_t *zone, dns_zone_t *raw) {
-	dns_zonemgr_t *zmgr;
-
 	REQUIRE(DNS_ZONE_VALID(zone));
-	REQUIRE(zone->zmgr != NULL);
 	REQUIRE(zone->loop != NULL);
 	REQUIRE(zone->raw == NULL);

 	REQUIRE(DNS_ZONE_VALID(raw));
-	REQUIRE(raw->zmgr == NULL);
-	REQUIRE(raw->loop == NULL);
 	REQUIRE(raw->secure == NULL);

 	REQUIRE(zone != raw);

 	/*
-	 * Lock hierarchy: zmgr, zone, raw.
+	 * Lock hierarchy: zone, raw. No global registration is needed.
 	 */
-	zmgr = zone->zmgr;
-	RWLOCK(&zmgr->rwlock, isc_rwlocktype_write);
 	LOCK_ZONE(zone);
 	LOCK_ZONE(raw);

+	REQUIRE(raw->timer == NULL);
+	if (raw->loop != NULL) {
+		isc_loop_detach(&raw->loop);
+	}
+	raw->tid = zone->tid;
 	isc_loop_attach(zone->loop, &raw->loop);

 	/* dns_zone_attach(raw, &zone->raw); */
@@ -19750,13 +19778,8 @@ dns_zone_link(dns_zone_t *zone, dns_zone_t *raw) {
 	/* dns_zone_iattach(zone, &raw->secure); */
 	zone_iattach(zone, &raw->secure);

-	ISC_LIST_APPEND(zmgr->zones, raw, link);
-	raw->zmgr = zmgr;
-	isc_refcount_increment(&zmgr->refs);
-
 	UNLOCK_ZONE(raw);
 	UNLOCK_ZONE(zone);
-	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_write);
 	return ISC_R_SUCCESS;
 }

diff --git a/lib/dns/zone_p.h b/lib/dns/zone_p.h
index 0ebdb6bc86..ae93941540 100644
--- a/lib/dns/zone_p.h
+++ b/lib/dns/zone_p.h
@@ -334,7 +334,7 @@ typedef struct zone_settimer {
 struct dns_zonemgr {
 	unsigned int magic;
 	isc_mem_t *mctx;
-	isc_refcount_t refs;
+	bool shuttingdown;
 	uint32_t workers;
 	isc_mem_t **mctxpool;
 	isc_ratelimiter_t *checkdsrl;
@@ -345,7 +345,6 @@ struct dns_zonemgr {
 	isc_rwlock_t rwlock;

 	/* Locked by rwlock. */
-	dns_zonelist_t zones;
 	dns_zonelist_t waiting_for_xfrin;
 	dns_zonelist_t xfrin_in_progress;

@@ -454,8 +453,6 @@ struct dns_zone {

 	isc_tid_t tid;
 	/* Locked */
-	dns_zonemgr_t *zmgr;
-	ISC_LINK(dns_zone_t) link; /* Used by zmgr. */
 	isc_loop_t *loop;
 	isc_timer_t *timer;
 	isc_refcount_t irefs;
diff --git a/lib/dns/zonemgr.c b/lib/dns/zonemgr.c
index ce21814716..3e47e7e712 100644
--- a/lib/dns/zonemgr.c
+++ b/lib/dns/zonemgr.c
@@ -26,8 +26,7 @@

 #include "zone_p.h"

-static void
-zonemgr_free(dns_zonemgr_t *zmgr);
+dns_zonemgr_t *dns_g_zonemgr = NULL;

 /***
  ***	Zone manager.
@@ -71,6 +70,7 @@ dns_zonemgr_create(isc_mem_t *mctx, dns_zonemgr_t **zmgrp) {
 	isc_loop_t *loop = isc_loop();

 	REQUIRE(mctx != NULL);
+	REQUIRE(dns_g_zonemgr == NULL);
 	REQUIRE(zmgrp != NULL && *zmgrp == NULL);

 	zmgr = isc_mem_get(mctx, sizeof(*zmgr));
@@ -81,10 +81,8 @@ dns_zonemgr_create(isc_mem_t *mctx, dns_zonemgr_t **zmgrp) {
 		.transfersperns = 2,
 	};

-	isc_refcount_init(&zmgr->refs, 1);
 	isc_mem_attach(mctx, &zmgr->mctx);

-	ISC_LIST_INIT(zmgr->zones);
 	ISC_LIST_INIT(zmgr->waiting_for_xfrin);
 	ISC_LIST_INIT(zmgr->xfrin_in_progress);
 	isc_rwlock_init(&zmgr->rwlock);
@@ -115,6 +113,7 @@ dns_zonemgr_create(isc_mem_t *mctx, dns_zonemgr_t **zmgrp) {

 	zmgr->magic = ZONEMGR_MAGIC;

+	dns_g_zonemgr = zmgr;
 	*zmgrp = zmgr;
 }

@@ -139,97 +138,16 @@ dns_zonemgr_createzone(dns_zonemgr_t *zmgr, dns_zone_t **zonep) {
 	}

 	dns_zone_create(&zone, mctx, tid);
+	isc_loop_attach(isc_loop_get(tid), &zone->loop);

 	*zonep = zone;

 	return ISC_R_SUCCESS;
 }

-isc_result_t
-dns_zonemgr_managezone(dns_zonemgr_t *zmgr, dns_zone_t *zone) {
-	REQUIRE(DNS_ZONE_VALID(zone));
-	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
-
-	RWLOCK(&zmgr->rwlock, isc_rwlocktype_write);
-	LOCK_ZONE(zone);
-	REQUIRE(zone->timer == NULL);
-	REQUIRE(zone->zmgr == NULL);
-
-	isc_loop_t *loop = isc_loop_get(zone->tid);
-	isc_loop_attach(loop, &zone->loop);
-
-	ISC_LIST_APPEND(zmgr->zones, zone, link);
-	zone->zmgr = zmgr;
-
-	isc_refcount_increment(&zmgr->refs);
-
-	UNLOCK_ZONE(zone);
-	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_write);
-	return ISC_R_SUCCESS;
-}
-
 void
-dns_zonemgr_releasezone(dns_zonemgr_t *zmgr, dns_zone_t *zone) {
-	REQUIRE(DNS_ZONE_VALID(zone));
+dns_zonemgr_resumexfrs(dns_zonemgr_t *zmgr) {
 	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
-	REQUIRE(zone->zmgr == zmgr);
-
-	RWLOCK(&zmgr->rwlock, isc_rwlocktype_write);
-	LOCK_ZONE(zone);
-
-	ISC_LIST_UNLINK(zmgr->zones, zone, link);
-
-	if (zone->timer != NULL) {
-		isc_refcount_decrement(&zone->irefs);
-		isc_timer_destroy(&zone->timer);
-	}
-
-	isc_loop_detach(&zone->loop);
-
-	/* Detach below, outside of the write lock. */
-	zone->zmgr = NULL;
-
-	UNLOCK_ZONE(zone);
-	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_write);
-
-	dns_zonemgr_detach(&zmgr);
-}
-
-void
-dns_zonemgr_attach(dns_zonemgr_t *source, dns_zonemgr_t **target) {
-	REQUIRE(DNS_ZONEMGR_VALID(source));
-	REQUIRE(target != NULL && *target == NULL);
-
-	isc_refcount_increment(&source->refs);
-
-	*target = source;
-}
-
-void
-dns_zonemgr_detach(dns_zonemgr_t **zmgrp) {
-	dns_zonemgr_t *zmgr;
-
-	REQUIRE(zmgrp != NULL);
-	zmgr = *zmgrp;
-	*zmgrp = NULL;
-	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
-
-	if (isc_refcount_decrement(&zmgr->refs) == 1) {
-		zonemgr_free(zmgr);
-	}
-}
-
-isc_result_t
-dns_zonemgr_forcemaint(dns_zonemgr_t *zmgr) {
-	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
-
-	RWLOCK(&zmgr->rwlock, isc_rwlocktype_read);
-	ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-		LOCK_ZONE(zone);
-		dns__zone_settimer(zone, isc_time_now());
-		UNLOCK_ZONE(zone);
-	}
-	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_read);

 	/*
 	 * Recent configuration changes may have increased the
@@ -240,12 +158,14 @@ dns_zonemgr_forcemaint(dns_zonemgr_t *zmgr) {
 	RWLOCK(&zmgr->rwlock, isc_rwlocktype_write);
 	dns__zonemgr_resume_xfrs(zmgr, true);
 	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_write);
-	return ISC_R_SUCCESS;
 }

 void
 dns_zonemgr_shutdown(dns_zonemgr_t *zmgr) {
 	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
+	REQUIRE(!zmgr->shuttingdown);
+
+	zmgr->shuttingdown = true;

 	isc_ratelimiter_shutdown(zmgr->checkdsrl);
 	isc_ratelimiter_shutdown(zmgr->notifyrl);
@@ -256,23 +176,22 @@ dns_zonemgr_shutdown(dns_zonemgr_t *zmgr) {
 	for (size_t i = 0; i < zmgr->workers; i++) {
 		isc_mem_detach(&zmgr->mctxpool[i]);
 	}
-
-	RWLOCK(&zmgr->rwlock, isc_rwlocktype_read);
-	ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-		LOCK_ZONE(zone);
-		dns__zone_forward_cancel(zone);
-		UNLOCK_ZONE(zone);
-	}
-	RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_read);
 }

-static void
-zonemgr_free(dns_zonemgr_t *zmgr) {
-	REQUIRE(ISC_LIST_EMPTY(zmgr->zones));
+void
+dns_zonemgr_destroy(dns_zonemgr_t **zmgrp) {
+	REQUIRE(zmgrp != NULL && DNS_ZONEMGR_VALID(*zmgrp));
+	REQUIRE(isc_loop() == NULL);
+	dns_zonemgr_t *zmgr = *zmgrp;
+	REQUIRE(zmgr->shuttingdown);
+	REQUIRE(zmgr == dns_g_zonemgr);
+	REQUIRE(ISC_LIST_EMPTY(zmgr->waiting_for_xfrin));
+	REQUIRE(ISC_LIST_EMPTY(zmgr->xfrin_in_progress));
+	*zmgrp = NULL;
+	dns_g_zonemgr = NULL;

 	zmgr->magic = 0;

-	isc_refcount_destroy(&zmgr->refs);
 	isc_ratelimiter_detach(&zmgr->checkdsrl);
 	isc_ratelimiter_detach(&zmgr->notifyrl);
 	isc_ratelimiter_detach(&zmgr->refreshrl);
@@ -528,7 +447,7 @@ got_transfer_quota(void *arg) {

 	INSIST(isc_sockaddr_pf(&primaryaddr) == isc_sockaddr_pf(&sourceaddr));

-	dns__zonemgr_tlsctx_attach(zone->zmgr, &zmgr_tlsctx_cache);
+	dns__zonemgr_tlsctx_attach(dns_g_zonemgr, &zmgr_tlsctx_cache);

 	dns_xfrin_create(zone, xfrtype, ixfr_maxdiffs, &primaryaddr,
 			 &sourceaddr, zone->tsigkey, soa_transport_type,
@@ -766,40 +685,6 @@ dns_zonemgr_getcount(dns_zonemgr_t *zmgr, dns_zonestate_t state) {
 			count++;
 		}
 		break;
-	case DNS_ZONESTATE_XFERFIRSTREFRESH:
-		ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-			if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_FIRSTREFRESH)) {
-				count++;
-			}
-		}
-		break;
-	case DNS_ZONESTATE_SOAQUERY:
-		ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-			if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_REFRESH)) {
-				count++;
-			}
-		}
-		break;
-	case DNS_ZONESTATE_ANY:
-		ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-			dns_view_t *view = zone->view;
-			if (view != NULL && strcmp(view->name, "_bind") == 0) {
-				continue;
-			}
-			count++;
-		}
-		break;
-	case DNS_ZONESTATE_AUTOMATIC:
-		ISC_LIST_FOREACH(zmgr->zones, zone, link) {
-			dns_view_t *view = zone->view;
-			if (view != NULL && strcmp(view->name, "_bind") == 0) {
-				continue;
-			}
-			if (zone->automatic) {
-				count++;
-			}
-		}
-		break;
 	default:
 		UNREACHABLE();
 	}
@@ -808,29 +693,3 @@ dns_zonemgr_getcount(dns_zonemgr_t *zmgr, dns_zonestate_t state) {

 	return count;
 }
-
-isc_result_t
-dns_zonemgr_next_zone(dns_zone_t *zone, dns_zone_t **next) {
-	REQUIRE(DNS_ZONE_VALID(zone));
-	REQUIRE(next != NULL && *next == NULL);
-
-	*next = ISC_LIST_NEXT(zone, link);
-	if (*next == NULL) {
-		return ISC_R_NOMORE;
-	} else {
-		return ISC_R_SUCCESS;
-	}
-}
-
-isc_result_t
-dns_zonemgr_first_zone(dns_zonemgr_t *zmgr, dns_zone_t **first) {
-	REQUIRE(DNS_ZONEMGR_VALID(zmgr));
-	REQUIRE(first != NULL && *first == NULL);
-
-	*first = ISC_LIST_HEAD(zmgr->zones);
-	if (*first == NULL) {
-		return ISC_R_NOMORE;
-	} else {
-		return ISC_R_SUCCESS;
-	}
-}
diff --git a/lib/dns/zoneproperties.c b/lib/dns/zoneproperties.c
index 292adbf28f..279ceebe30 100644
--- a/lib/dns/zoneproperties.c
+++ b/lib/dns/zoneproperties.c
@@ -24,6 +24,7 @@
 #include <dns/stats.h>
 #include <dns/view.h>
 #include <dns/zone.h>
+#include <dns/zonemgr.h>
 #include <dns/zoneproperties.h>

 #include "zone_p.h"
@@ -590,13 +591,6 @@ dns_zone_getmctx(dns_zone_t *zone) {
 	return zone->mctx;
 }

-dns_zonemgr_t *
-dns_zone_getmgr(dns_zone_t *zone) {
-	REQUIRE(DNS_ZONE_VALID(zone));
-
-	return zone->zmgr;
-}
-
 void
 dns_zone_setkasp(dns_zone_t *zone, dns_kasp_t *kasp) {
 	REQUIRE(DNS_ZONE_VALID(zone));
@@ -2123,7 +2117,8 @@ dns_zone_getrequesttransporttype(dns_zone_t *zone) {

 dns_keystorelist_t *
 dns_zone_getkeystores(dns_zone_t *zone) {
-	return zone->zmgr->keystores;
+	REQUIRE(DNS_ZONE_VALID(zone));
+	return dns_g_zonemgr->keystores;
 }

 isc_stats_t *
diff --git a/tests/dns/update_test.c b/tests/dns/update_test.c
index 14c6f6be36..973b019600 100644
--- a/tests/dns/update_test.c
+++ b/tests/dns/update_test.c
@@ -397,8 +397,6 @@ ISC_LOOP_TEST_IMPL(find_zone_keys_overflow) {
 	dns_zonemgr_create(isc_g_mctx, &mgr);
 	result = dns_test_makezone("example", &zone, NULL, false);
 	assert_int_equal(result, ISC_R_SUCCESS);
-	result = dns_zonemgr_managezone(mgr, zone);
-	assert_int_equal(result, ISC_R_SUCCESS);
 	dns_zone_setkeydirectory(zone, keydir);

 	result = find_zone_keys(zone, isc_g_mctx, DNS_MAXZONEKEYS, keys,
@@ -415,10 +413,8 @@ ISC_LOOP_TEST_IMPL(find_zone_keys_overflow) {
 		dst_key_free(&keys[i]);
 	}

-	dns_zonemgr_releasezone(mgr, zone);
 	dns_zone_detach(&zone);
 	dns_zonemgr_shutdown(mgr);
-	dns_zonemgr_detach(&mgr);

 	cleanup_keydir(keydir);

@@ -427,6 +423,13 @@ ISC_LOOP_TEST_IMPL(find_zone_keys_overflow) {
 	isc_loopmgr_shutdown();
 }

+static int
+teardown_zone_services(void **state) {
+	teardown_managers(state);
+	dns_zonemgr_destroy(&dns_g_zonemgr);
+	return 0;
+}
+
 ISC_TEST_LIST_START
 ISC_TEST_ENTRY_CUSTOM(increment, setup_test, NULL)
 ISC_TEST_ENTRY_CUSTOM(increment_past_zero, setup_test, NULL)
@@ -441,7 +444,7 @@ ISC_TEST_ENTRY_CUSTOM(past_to_date, setup_test, NULL)
 ISC_TEST_ENTRY_CUSTOM(now_to_date, setup_test, NULL)
 ISC_TEST_ENTRY_CUSTOM(future_to_date, setup_test, NULL)
 ISC_TEST_ENTRY_CUSTOM(find_zone_keys_overflow, setup_managers,
-		      teardown_managers)
+		      teardown_zone_services)
 ISC_TEST_LIST_END

 ISC_TEST_MAIN
diff --git a/tests/dns/zonemgr_test.c b/tests/dns/zonemgr_test.c
index 26594e67b9..d6ed24fcf1 100644
--- a/tests/dns/zonemgr_test.c
+++ b/tests/dns/zonemgr_test.c
@@ -37,6 +37,8 @@

 #include <tests/dns.h>

+static dns_zonemgr_t *mgr = NULL;
+
 static int
 setup_test(void **state) {
 	setup_loopmgr(state);
@@ -49,88 +51,80 @@ static int
 teardown_test(void **state) {
 	teardown_netmgr(state);
 	teardown_loopmgr(state);
+	assert_ptr_equal(dns_g_zonemgr, mgr);
+	dns_zonemgr_destroy(&mgr);
+	assert_null(mgr);
+	assert_null(dns_g_zonemgr);

 	return 0;
 }

 /* create zone manager */
 ISC_LOOP_TEST_IMPL(zonemgr_create) {
-	dns_zonemgr_t *myzonemgr = NULL;
-
 	UNUSED(arg);

-	dns_zonemgr_create(isc_g_mctx, &myzonemgr);
+	dns_zonemgr_create(isc_g_mctx, &mgr);

-	dns_zonemgr_shutdown(myzonemgr);
-	dns_zonemgr_detach(&myzonemgr);
-	assert_null(myzonemgr);
+	dns_zonemgr_shutdown(mgr);
+	assert_ptr_equal(dns_g_zonemgr, mgr);

 	isc_loopmgr_shutdown();
 }

-/* manage and release a zone */
-ISC_LOOP_TEST_IMPL(zonemgr_managezone) {
-	dns_zonemgr_t *myzonemgr = NULL;
+/* create and release a zone */
+ISC_LOOP_TEST_IMPL(zonemgr_createzone) {
 	dns_zone_t *zone = NULL;
 	isc_result_t result;

 	UNUSED(arg);

-	dns_zonemgr_create(isc_g_mctx, &myzonemgr);
+	dns_zonemgr_create(isc_g_mctx, &mgr);

-	result = dns_test_makezone("foo", &zone, NULL, false);
-	assert_int_equal(result, ISC_R_SUCCESS);
-
-	assert_int_equal(dns_zonemgr_getcount(myzonemgr, DNS_ZONESTATE_ANY), 0);
-
-	/* Now it should succeed */
-	result = dns_zonemgr_managezone(myzonemgr, zone);
+	result = dns_zonemgr_createzone(mgr, &zone);
 	assert_int_equal(result, ISC_R_SUCCESS);
+	assert_non_null(zone);

-	assert_int_equal(dns_zonemgr_getcount(myzonemgr, DNS_ZONESTATE_ANY), 1);
+	assert_non_null(dns_zone_getloop(zone));

-	dns_zonemgr_releasezone(myzonemgr, zone);
 	dns_zone_detach(&zone);

-	assert_int_equal(dns_zonemgr_getcount(myzonemgr, DNS_ZONESTATE_ANY), 0);
-
-	dns_zonemgr_shutdown(myzonemgr);
-	dns_zonemgr_detach(&myzonemgr);
-	assert_null(myzonemgr);
+	dns_zonemgr_shutdown(mgr);
+	assert_ptr_equal(dns_g_zonemgr, mgr);

 	isc_loopmgr_shutdown();
 }

-/* create and release a zone */
-ISC_LOOP_TEST_IMPL(zonemgr_createzone) {
-	dns_zonemgr_t *myzonemgr = NULL;
-	dns_zone_t *zone = NULL;
-	isc_result_t result;
-
+/* Both raw-zone construction paths must finish cleanup before destruction. */
+ISC_LOOP_TEST_IMPL(zonemgr_inline) {
+	dns_zone_t *zone = NULL, *raw = NULL;
 	UNUSED(arg);
+	dns_zonemgr_create(isc_g_mctx, &mgr);

-	dns_zonemgr_create(isc_g_mctx, &myzonemgr);
-
-	result = dns_zonemgr_createzone(myzonemgr, &zone);
-	assert_int_equal(result, ISC_R_SUCCESS);
-	assert_non_null(zone);
-
-	result = dns_zonemgr_managezone(myzonemgr, zone);
-	assert_int_equal(result, ISC_R_SUCCESS);
-
+	assert_int_equal(dns_zonemgr_createzone(mgr, &zone), ISC_R_SUCCESS);
+	dns_zone_create(&raw, isc_g_mctx, 0);
+	assert_int_equal(dns_zone_link(zone, raw), ISC_R_SUCCESS);
+	assert_ptr_equal(dns_zone_getloop(zone), dns_zone_getloop(raw));
+	dns_zone_detach(&raw);
 	dns_zone_detach(&zone);

-	dns_zonemgr_shutdown(myzonemgr);
-	dns_zonemgr_detach(&myzonemgr);
-	assert_null(myzonemgr);
+	assert_int_equal(dns_zonemgr_createzone(mgr, &zone), ISC_R_SUCCESS);
+	assert_int_equal(dns_zonemgr_createzone(mgr, &raw), ISC_R_SUCCESS);
+	assert_int_equal(dns_zone_link(zone, raw), ISC_R_SUCCESS);
+	assert_ptr_equal(dns_zone_getloop(zone), dns_zone_getloop(raw));

+	dns_zonemgr_shutdown(mgr);
+	/* Shutdown leaves shared services allocated while zones finish cleanup.
+	 */
+	assert_non_null(dns_g_zonemgr);
+	dns_zone_detach(&raw);
+	dns_zone_detach(&zone);
 	isc_loopmgr_shutdown();
 }

 ISC_TEST_LIST_START
 ISC_TEST_ENTRY_CUSTOM(zonemgr_create, setup_test, teardown_test)
-ISC_TEST_ENTRY_CUSTOM(zonemgr_managezone, setup_test, teardown_test)
 ISC_TEST_ENTRY_CUSTOM(zonemgr_createzone, setup_test, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(zonemgr_inline, setup_test, teardown_test)
 ISC_TEST_LIST_END

 ISC_TEST_MAIN
diff --git a/tests/dns/zt_test.c b/tests/dns/zt_test.c
index 539ad0ced6..26d994c3b2 100644
--- a/tests/dns/zt_test.c
+++ b/tests/dns/zt_test.c
@@ -46,6 +46,13 @@ static dns_db_t *db = NULL;
 static FILE *zonefile, *origfile;
 static dns_view_t *view = NULL;

+static int
+teardown_test(void **state) {
+	teardown_managers(state);
+	dns_test_destroyzonemgr();
+	return 0;
+}
+
 static isc_result_t
 count_zone(dns_zone_t *zone, void *uap) {
 	int *nzones = (int *)uap;
@@ -78,13 +85,6 @@ ISC_LOOP_TEST_IMPL(apply) {
 	assert_int_equal(result, ISC_R_SUCCESS);
 	assert_int_equal(nzones, 1);

-	/* These steps are necessary so the zone can be detached properly */
-	dns_test_setupzonemgr();
-	result = dns_test_managezone(zone);
-	assert_int_equal(result, ISC_R_SUCCESS);
-	dns_test_releasezone(zone);
-	dns_test_closezonemgr();
-
 	/* The view was left attached in dns_test_makezone() */
 	dns_view_detach(&view);
 	dns_zone_detach(&zone);
@@ -105,7 +105,6 @@ load_done_last(void *uap) {
 		dns_db_detach(&db);
 	}

-	dns_test_releasezone(zone);
 	dns_test_closezonemgr();

 	dns_zone_detach(&zone);
@@ -156,6 +155,7 @@ load_done_first(void *uap) {

 /* asynchronous zone load */
 ISC_LOOP_TEST_IMPL(asyncload_zone) {
+	dns_test_setupzonemgr();
 	isc_result_t result;
 	int n;
 	dns_zone_t *zone = NULL;
@@ -165,10 +165,6 @@ ISC_LOOP_TEST_IMPL(asyncload_zone) {
 	result = dns_test_makezone("foo", &zone, NULL, true);
 	assert_int_equal(result, ISC_R_SUCCESS);

-	dns_test_setupzonemgr();
-	result = dns_test_managezone(zone);
-	assert_int_equal(result, ISC_R_SUCCESS);
-
 	view = dns_zone_getview(zone);
 	rcu_read_lock();
 	zt = rcu_dereference(view->zonetable);
@@ -212,9 +208,6 @@ all_done(void *arg ISC_ATTR_UNUSED) {
 		dns_db_detach(&db);
 	}

-	dns_test_releasezone(zone3);
-	dns_test_releasezone(zone2);
-	dns_test_releasezone(zone1);
 	dns_test_closezonemgr();

 	dns_zone_detach(&zone1);
@@ -232,6 +225,7 @@ ISC_LOOP_TEST_IMPL(asyncload_zt) {
 	dns_zt_t *zt = NULL;
 	atomic_bool done;

+	dns_test_setupzonemgr();
 	atomic_init(&done, false);

 	result = dns_test_makezone("foo", &zone1, NULL, true);
@@ -256,14 +250,6 @@ ISC_LOOP_TEST_IMPL(asyncload_zt) {
 	rcu_read_unlock();
 	assert_non_null(zt);

-	dns_test_setupzonemgr();
-	result = dns_test_managezone(zone1);
-	assert_int_equal(result, ISC_R_SUCCESS);
-	result = dns_test_managezone(zone2);
-	assert_int_equal(result, ISC_R_SUCCESS);
-	result = dns_test_managezone(zone3);
-	assert_int_equal(result, ISC_R_SUCCESS);
-
 	assert_false(dns__zone_loadpending(zone1));
 	assert_false(dns__zone_loadpending(zone2));
 	assert_false(atomic_load(&done));
@@ -275,9 +261,9 @@ ISC_LOOP_TEST_IMPL(asyncload_zt) {
 }

 ISC_TEST_LIST_START
-ISC_TEST_ENTRY_CUSTOM(apply, setup_managers, teardown_managers)
-ISC_TEST_ENTRY_CUSTOM(asyncload_zone, setup_managers, teardown_managers)
-ISC_TEST_ENTRY_CUSTOM(asyncload_zt, setup_managers, teardown_managers)
+ISC_TEST_ENTRY_CUSTOM(apply, setup_managers, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(asyncload_zone, setup_managers, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(asyncload_zt, setup_managers, teardown_test)
 ISC_TEST_LIST_END

 ISC_TEST_MAIN
diff --git a/tests/include/tests/dns.h b/tests/include/tests/dns.h
index 2ce75fcc96..7ff9a0fab7 100644
--- a/tests/include/tests/dns.h
+++ b/tests/include/tests/dns.h
@@ -73,14 +73,13 @@ dns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view,
 void
 dns_test_setupzonemgr(void);

-isc_result_t
-dns_test_managezone(dns_zone_t *zone);
-
+/* Stop services while the event loops are running. */
 void
-dns_test_releasezone(dns_zone_t *zone);
+dns_test_closezonemgr(void);

+/* Destroy services after the event loops have drained. */
 void
-dns_test_closezonemgr(void);
+dns_test_destroyzonemgr(void);

 void
 dns_test_nap(uint32_t usec);
diff --git a/tests/libtest/dns.c b/tests/libtest/dns.c
index e98b0e15b1..098b68445a 100644
--- a/tests/libtest/dns.c
+++ b/tests/libtest/dns.c
@@ -112,7 +112,11 @@ dns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view,
 	/*
 	 * Create the zone structure.
 	 */
-	dns_zone_create(&zone, isc_g_mctx, 0);
+	if (zonemgr != NULL) {
+		RETERR(dns_zonemgr_createzone(zonemgr, &zone));
+	} else {
+		dns_zone_create(&zone, isc_g_mctx, 0);
+	}

 	/*
 	 * Set zone type and origin.
@@ -164,27 +168,18 @@ dns_test_setupzonemgr(void) {
 	dns_zonemgr_create(isc_g_mctx, &zonemgr);
 }

-isc_result_t
-dns_test_managezone(dns_zone_t *zone) {
-	isc_result_t result;
-	REQUIRE(zonemgr != NULL);
-
-	result = dns_zonemgr_managezone(zonemgr, zone);
-	return result;
-}
-
-void
-dns_test_releasezone(dns_zone_t *zone) {
-	REQUIRE(zonemgr != NULL);
-	dns_zonemgr_releasezone(zonemgr, zone);
-}
-
 void
 dns_test_closezonemgr(void) {
 	REQUIRE(zonemgr != NULL);

 	dns_zonemgr_shutdown(zonemgr);
-	dns_zonemgr_detach(&zonemgr);
+}
+
+void
+dns_test_destroyzonemgr(void) {
+	if (zonemgr != NULL) {
+		dns_zonemgr_destroy(&zonemgr);
+	}
 }

 /*
diff --git a/tests/libtest/ns.c b/tests/libtest/ns.c
index 8b2ace5959..fb149c19a0 100644
--- a/tests/libtest/ns.c
+++ b/tests/libtest/ns.c
@@ -87,6 +87,16 @@ scan_interfaces(void *arg) {
 	ns_interfacemgr_scan(interfacemgr, true, false);
 }

+static void
+setup_zone_services(void *arg ISC_ATTR_UNUSED) {
+	dns_test_setupzonemgr();
+}
+
+static void
+shutdown_zone_services(void *arg ISC_ATTR_UNUSED) {
+	dns_test_closezonemgr();
+}
+
 int
 setup_server(void **state) {
 	isc_result_t result;
@@ -99,7 +109,9 @@ setup_server(void **state) {
 	CHECK(ns_interfacemgr_create(isc_g_mctx, sctx, dispatchmgr, NULL,
 				     &interfacemgr));

+	isc_loop_setup(isc_loop_main(), setup_zone_services, NULL);
 	isc_loop_setup(isc_loop_main(), scan_interfaces, NULL);
+	isc_loop_teardown(isc_loop_main(), shutdown_zone_services, NULL);

 	return 0;

@@ -129,15 +141,22 @@ teardown_server(void **state) {
 	}

 	teardown_managers(state);
+	dns_test_destroyzonemgr();
 	return 0;
 }

 static dns_zone_t *served_zone = NULL;

+static void
+free_test_zone_hooks(isc_mem_t *mctx ISC_ATTR_UNUSED, void **tablep) {
+	/* Test hook tables are allocated from isc_g_mctx, not the zone pool. */
+	ns_hooktable_free(isc_g_mctx, tablep);
+}
+
 void
 ns_test_serve_zone_sethooktab(ns_hooktable_t *hooktab) {
 	static const dns_zone_ops_t ops = {
-		.hooktable_free = ns_hooktable_free,
+		.hooktable_free = free_test_zone_hooks,
 	};

 	dns_zone_setops(served_zone, &ops);
@@ -150,24 +169,8 @@ ns_test_serve_zone(const char *zonename, const char *filename,
 	isc_result_t result;
 	dns_db_t *db = NULL;

-	/*
-	 * Prepare zone structure for further processing.
-	 */
 	RETERR(dns_test_makezone(zonename, &served_zone, view, false));

-	/*
-	 * Start zone manager.
-	 */
-	dns_test_setupzonemgr();
-
-	/*
-	 * Add the zone to the zone manager.
-	 */
-	result = dns_test_managezone(served_zone);
-	if (result != ISC_R_SUCCESS) {
-		goto close_zonemgr;
-	}
-
 	view->nocookieudp = 512;

 	/*
@@ -194,19 +197,15 @@ ns_test_serve_zone(const char *zonename, const char *filename,
 	return ISC_R_SUCCESS;

 release_zone:
-	dns_test_releasezone(served_zone);
-close_zonemgr:
-	dns_test_closezonemgr();
-	dns_zone_detach(&served_zone);
+	if (served_zone != NULL) {
+		dns_zone_detach(&served_zone);
+	}

 	return result;
 }

 void
 ns_test_cleanup_zone(void) {
-	dns_test_releasezone(served_zone);
-	dns_test_closezonemgr();
-
 	dns_zone_detach(&served_zone);
 }