Commit 03c0e781a2 for bind

commit 03c0e781a210cf599f96c86bfccc9b8d070b9fa3
Author: Alessio Podda <alessio@isc.org>
Date:   Fri Sep 25 15:46:35 2026 +0200

    Fix race condition at zone shutdown

    DB verification functions would access the zone for logging in
    the case of IXFR to mirror zones, and that could happen even when
    the zone was shut down.

    The zone was only used to access the view, pass the view directly
    instead.

diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h
index 0f605f7f6c..633315add0 100644
--- a/lib/dns/include/dns/zone.h
+++ b/lib/dns/include/dns/zone.h
@@ -926,22 +926,17 @@ dns_zone_isloaded(dns_zone_t *zone);
  */

 isc_result_t
-dns_zone_verifydb(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver);
+dns_zone_verifydb(dns_view_t *view, dns_db_t *db, dns_dbversion_t *ver);
 /*%<
- * If 'zone' is a mirror zone, perform DNSSEC validation of version 'ver' of
- * its database, 'db'.  Ensure that the DNSKEY RRset at zone apex is signed by
- * at least one trust anchor specified for the view that 'zone' is assigned to.
- * If 'ver' is NULL, use the current version of 'db'.
- *
- * If 'zone' is not a mirror zone, return ISC_R_SUCCESS immediately.
+ * Perform DNSSEC validation of version 'ver' of database 'db'.  Ensure that
+ * the DNSKEY RRset at zone apex is signed by at least one trust anchor in
+ * 'view', if non-NULL.  If 'ver' is NULL, use the current version of 'db'.
+ * The caller must keep 'view' alive (a weak reference suffices) throughout
+ * the call, and is responsible for deciding whether validation is required.
  *
  * Returns:
  *
- * \li	#ISC_R_SUCCESS		either 'zone' is not a mirror zone or 'zone' is
- *				a mirror zone and all DNSSEC checks succeeded
- *				and the DNSKEY RRset at zone apex is signed by
- *				a trusted key
- *
+ * \li	#ISC_R_SUCCESS		all DNSSEC checks succeeded
  * \li	#DNS_R_VERIFYFAILURE	any other case
  */

diff --git a/lib/dns/include/dns/zoneverify.h b/lib/dns/include/dns/zoneverify.h
index 77f5d97eb2..1d3b7defda 100644
--- a/lib/dns/include/dns/zoneverify.h
+++ b/lib/dns/include/dns/zoneverify.h
@@ -22,6 +22,8 @@
 #include <dns/types.h>

 /*%
+ * Use 'zonename' as the logging prefix, or stderr if NULL.
+ *
  * Verify that certain things are sane:
  *
  *   The apex has a DNSKEY record with at least one KSK, and at least
@@ -40,7 +42,7 @@
  * correctly signed by at least one key present in 'secroots'.
  */
 isc_result_t
-dns_zoneverify_dnssec(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver,
+dns_zoneverify_dnssec(const char *zonename, dns_db_t *db, dns_dbversion_t *ver,
 		      dns_name_t *origin, dns_keytable_t *secroots,
 		      isc_mem_t *mctx, bool ignore_kskflag, bool keyset_kskonly,
 		      void (*report)(const char *, ...));
diff --git a/lib/dns/meson.build b/lib/dns/meson.build
index 8ba0a942e8..2e85ae277a 100644
--- a/lib/dns/meson.build
+++ b/lib/dns/meson.build
@@ -78,6 +78,7 @@ dns_srcset.add(

 dns_srcset.add(
     files(
+        'zone/viewname.c',
         'acl.c',
         'adb.c',
         'badcache.c',
diff --git a/lib/dns/xfrin.c b/lib/dns/xfrin.c
index b85306e161..555bf21be7 100644
--- a/lib/dns/xfrin.c
+++ b/lib/dns/xfrin.c
@@ -366,7 +366,9 @@ axfr_apply_done(void *arg, isc_result_t result) {

 	if (result == ISC_R_SUCCESS) {
 		CHECK(dns_db_endload(xfr->db, &xfr->axfr));
-		CHECK(dns_zone_verifydb(xfr->zone, xfr->db, NULL));
+		if (dns_zone_gettype(xfr->zone) == dns_zone_mirror) {
+			CHECK(dns_zone_verifydb(xfr->view, xfr->db, NULL));
+		}
 		CHECK(axfr_finalize(xfr));
 	} else {
 		(void)dns_db_endload(xfr->db, &xfr->axfr);
@@ -524,7 +526,9 @@ ixfr_apply_one(dns_xfrin_t *xfr, ixfr_apply_data_t *data) {
 	 */
 	dns_db_commitupdate(xfr->db, &callbacks);

-	CHECK(dns_zone_verifydb(xfr->zone, xfr->db, xfr->ver));
+	if (dns_zone_gettype(xfr->zone) == dns_zone_mirror) {
+		CHECK(dns_zone_verifydb(xfr->view, xfr->db, xfr->ver));
+	}

 	result = ixfr_end_transaction(&xfr->ixfr);

diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index d86a9a0ee5..357f8fb6fd 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -701,6 +701,7 @@ dns__zone_free(dns_zone_t *zone) {
 	}

 	dns_zone_setrad(zone, NULL);
+	dns__viewname_free(&zone->viewname, zone->mctx);

 	if (zone->ssutable != NULL) {
 		dns_ssutable_detach(&zone->ssutable);
@@ -841,6 +842,7 @@ dns__zone_freedbargs(dns_zone_t *zone) {

 void
 dns__zone_setview_helper(dns_zone_t *zone, dns_view_t *view) {
+	dns__viewname_set(&zone->viewname, zone->mctx, view->name);
 	if (zone->prev_view == NULL && zone->view != NULL) {
 		dns_view_weakattach(zone->view, &zone->prev_view);
 	}
@@ -4232,7 +4234,9 @@ zone_postload(dns_zone_t *zone, dns_db_t *db, isc_time_t loadtime,
 			CLEANUP(DNS_R_BADZONE);
 		}

-		CHECK(dns_zone_verifydb(zone, db, NULL));
+		if (zone->type == dns_zone_mirror) {
+			CHECK(dns_zone_verifydb(zone->view, db, NULL));
+		}

 		if (zone->db != NULL) {
 			unsigned int oldsoacount;
@@ -16234,12 +16238,12 @@ zone_namerd_tostr(dns_zone_t *zone, char *buf, size_t length) {
 		(void)dns_rdataclass_totext(zone->rdclass, &buffer);
 	}

-	if (zone->view != NULL && strcmp(zone->view->name, "_bind") != 0 &&
-	    strcmp(zone->view->name, "_default") != 0 &&
-	    strlen(zone->view->name) < isc_buffer_availablelength(&buffer))
+	const char *viewname = dns__viewname_display(&zone->viewname);
+	if (viewname != NULL &&
+	    strlen(viewname) < isc_buffer_availablelength(&buffer))
 	{
 		isc_buffer_putstr(&buffer, "/");
-		isc_buffer_putstr(&buffer, zone->view->name);
+		isc_buffer_putstr(&buffer, viewname);
 	}
 	/* Logging also runs without the zone lock.  These configuration
 	 * fields are changed before publication or with exclusive access. */
@@ -20715,21 +20719,35 @@ dns_zone_isloaded(dns_zone_t *zone) {
 	return DNS_ZONE_FLAG(zone, DNS_ZONEFLG_LOADED);
 }

+static void
+db_namerd_tostr(dns_db_t *db, dns_view_t *view, char *buf, size_t length) {
+	char originbuf[DNS_NAME_FORMATSIZE];
+	char classbuf[DNS_RDATACLASS_FORMATSIZE];
+	const char *viewname = "";
+
+	dns_name_format(dns_db_origin(db), originbuf, sizeof(originbuf));
+	dns_rdataclass_format(dns_db_class(db), classbuf, sizeof(classbuf));
+	if (view != NULL && strcmp(view->name, "_bind") != 0 &&
+	    strcmp(view->name, "_default") != 0)
+	{
+		viewname = view->name;
+	}
+	snprintf(buf, length, "%.*s/%s%s%.1000s", DNS_NAME_MAXTEXT, originbuf,
+		 classbuf, viewname[0] != '\0' ? "/" : "", viewname);
+}
+
 isc_result_t
-dns_zone_verifydb(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver) {
+dns_zone_verifydb(dns_view_t *view, dns_db_t *db, dns_dbversion_t *ver) {
 	dns_dbversion_t *version = NULL;
 	dns_keytable_t *secroots = NULL;
 	isc_result_t result;
 	dns_name_t *origin;
+	char name[2048];

-	REQUIRE(DNS_ZONE_VALID(zone));
+	REQUIRE(view == NULL || DNS_VIEW_VALID(view));
 	REQUIRE(db != NULL);

-	ENTER;
-
-	if (dns_zone_gettype(zone) != dns_zone_mirror) {
-		return ISC_R_SUCCESS;
-	}
+	db_namerd_tostr(db, view, name, sizeof(name));

 	if (ver == NULL) {
 		dns_db_currentversion(db, &version);
@@ -20737,14 +20755,14 @@ dns_zone_verifydb(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver) {
 		version = ver;
 	}

-	if (zone->view != NULL) {
-		result = dns_view_getsecroots(zone->view, &secroots);
+	if (view != NULL) {
+		result = dns_view_getsecroots(view, &secroots);
 		CHECK(result);
 	}

 	origin = dns_db_origin(db);
-	result = dns_zoneverify_dnssec(zone, db, version, origin, secroots,
-				       zone->mctx, true, false, dnssec_report);
+	result = dns_zoneverify_dnssec(name, db, version, origin, secroots,
+				       db->mctx, true, false, dnssec_report);

 cleanup:
 	if (secroots != NULL) {
@@ -20756,8 +20774,10 @@ cleanup:
 	}

 	if (result != ISC_R_SUCCESS) {
-		dnssec_log(zone, ISC_LOG_ERROR, "zone verification failed: %s",
-			   isc_result_totext(result));
+		isc_log_write(DNS_LOGCATEGORY_DNSSEC, DNS_LOGMODULE_ZONE,
+			      ISC_LOG_ERROR,
+			      "zone %s: zone verification failed: %s", name,
+			      isc_result_totext(result));
 		result = DNS_R_VERIFYFAILURE;
 	}

diff --git a/lib/dns/zone/viewname.c b/lib/dns/zone/viewname.c
new file mode 100644
index 0000000000..3b673697d5
--- /dev/null
+++ b/lib/dns/zone/viewname.c
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <string.h>
+
+#include <isc/mem.h>
+#include <isc/util.h>
+
+#include "viewname_p.h"
+
+static const char default_view[] = "_default";
+static const char bind_view[] = "_bind";
+
+const char *
+dns__viewname_get(const dns_viewname_t *name) {
+	return name->name;
+}
+
+const char *
+dns__viewname_display(const dns_viewname_t *name) {
+	if (name->name == default_view || name->name == bind_view) {
+		return NULL;
+	}
+	return name->name;
+}
+
+void
+dns__viewname_free(dns_viewname_t *name, isc_mem_t *mctx) {
+	if (dns__viewname_display(name) != NULL) {
+		char *value = (char *)name->name;
+		isc_mem_free(mctx, value);
+	}
+	name->name = NULL;
+}
+
+void
+dns__viewname_set(dns_viewname_t *name, isc_mem_t *mctx, const char *value) {
+	REQUIRE(value != NULL);
+
+	/* Reattaching a view of the same name leaves logging storage intact. */
+	if (name->name != NULL && strcmp(name->name, value) == 0) {
+		return;
+	}
+
+	const char *replacement = NULL;
+	if (value[0] == '_' && strcmp(value, default_view) == 0) {
+		replacement = default_view;
+	} else if (value[0] == '_' && strcmp(value, bind_view) == 0) {
+		replacement = bind_view;
+	} else {
+		replacement = isc_mem_strdup(mctx, value);
+	}
+	dns__viewname_free(name, mctx);
+	name->name = replacement;
+}
diff --git a/lib/dns/zone/viewname_p.h b/lib/dns/zone/viewname_p.h
new file mode 100644
index 0000000000..b0b6fb4da3
--- /dev/null
+++ b/lib/dns/zone/viewname_p.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#pragma once
+
+#include <isc/types.h>
+
+/*
+ * A zone-owned view name.  Built-in names share static storage; custom names
+ * are copied.  Initialize to zero, retain through shutdown, and free only at
+ * final zone destruction.  Setters require exclusive access to readers.
+ */
+typedef struct {
+	const char *name;
+} dns_viewname_t;
+
+void
+dns__viewname_set(dns_viewname_t *name, isc_mem_t *mctx, const char *value);
+
+void
+dns__viewname_free(dns_viewname_t *name, isc_mem_t *mctx);
+
+/* Return the full name, or NULL if no view has been assigned. */
+const char *
+dns__viewname_get(const dns_viewname_t *name);
+
+/* Return the logging suffix name, or NULL for absent/built-in views. */
+const char *
+dns__viewname_display(const dns_viewname_t *name);
diff --git a/lib/dns/zone_p.h b/lib/dns/zone_p.h
index 505263dd13..0ebdb6bc86 100644
--- a/lib/dns/zone_p.h
+++ b/lib/dns/zone_p.h
@@ -30,6 +30,8 @@
 #include <dns/zoneaddr.h>
 #include <dns/zonefetch.h>

+#include "zone/viewname_p.h"
+
 /*%
  *	Types and functions below meant to be used for internal zone
  *	modules only, and associated unit tests.
@@ -546,6 +548,7 @@ struct dns_zone {
 	uint32_t sigvalidityinterval;
 	uint32_t keyvalidityinterval;
 	uint32_t sigresigninginterval;
+	dns_viewname_t viewname;
 	dns_view_t *view;
 	dns_view_t *prev_view;
 	dns_kasp_t *kasp;
diff --git a/lib/dns/zoneproperties.c b/lib/dns/zoneproperties.c
index 547b7c43ca..19b1c12fe4 100644
--- a/lib/dns/zoneproperties.c
+++ b/lib/dns/zoneproperties.c
@@ -476,7 +476,7 @@ setfilename(dns_zone_t *zone, char **field, const char *value) {

 	isc_buffer_init(&b, filename, sizeof(filename));
 	dns_zone_expandzonefile(&b, value, &zone->origin,
-				zone->view != NULL ? zone->view->name : NULL,
+				dns__viewname_get(&zone->viewname),
 				dns_zonetype_name(zone->type));
 	setstring(zone, field, filename);
 }
@@ -1287,12 +1287,12 @@ zone_namerd_tostr(dns_zone_t *zone, char *buf, size_t length) {
 		(void)dns_rdataclass_totext(zone->rdclass, &buffer);
 	}

-	if (zone->view != NULL && strcmp(zone->view->name, "_bind") != 0 &&
-	    strcmp(zone->view->name, "_default") != 0 &&
-	    strlen(zone->view->name) < isc_buffer_availablelength(&buffer))
+	const char *viewname = dns__viewname_display(&zone->viewname);
+	if (viewname != NULL &&
+	    strlen(viewname) < isc_buffer_availablelength(&buffer))
 	{
 		isc_buffer_putstr(&buffer, "/");
-		isc_buffer_putstr(&buffer, zone->view->name);
+		isc_buffer_putstr(&buffer, viewname);
 	}
 	if (dns__zone_inline_secure(zone) &&
 	    9U < isc_buffer_availablelength(&buffer))
diff --git a/lib/dns/zoneverify.c b/lib/dns/zoneverify.c
index 7be5c7feea..6969cd2b3e 100644
--- a/lib/dns/zoneverify.c
+++ b/lib/dns/zoneverify.c
@@ -46,14 +46,13 @@
 #include <dns/rdatatype.h>
 #include <dns/secalg.h>
 #include <dns/types.h>
-#include <dns/zone.h>
 #include <dns/zoneverify.h>

 #include <dst/dst.h>

 typedef struct vctx {
 	isc_mem_t *mctx;
-	dns_zone_t *zone;
+	const char *zonename;
 	dns_db_t *db;
 	dns_dbversion_t *ver;
 	dns_name_t *origin;
@@ -107,18 +106,21 @@ chain_length(struct nsec3_chain_fixed *chain) {

 /*%
  * Log a zone verification error described by 'fmt' and the variable arguments
- * following it.  Either use dns_zone_logv() or print to stderr, depending on
- * whether the function was invoked from within named or by a standalone tool,
- * respectively.
+ * following it.  Either use the supplied zone name or print to stderr,
+ * depending on whether the function was invoked from within named or by a
+ * standalone tool, respectively.
  */
 static void
 zoneverify_log_error(const vctx_t *vctx, const char *fmt, ...) {
 	va_list ap;

 	va_start(ap, fmt);
-	if (vctx->zone != NULL) {
-		dns_zone_logv(vctx->zone, DNS_LOGCATEGORY_GENERAL,
-			      ISC_LOG_ERROR, NULL, fmt, ap);
+	if (vctx->zonename != NULL) {
+		char message[4096];
+		vsnprintf(message, sizeof(message), fmt, ap);
+		isc_log_write(DNS_LOGCATEGORY_GENERAL, DNS_LOGMODULE_ZONE,
+			      ISC_LOG_ERROR, "zone %s: %s", vctx->zonename,
+			      message);
 	} else {
 		vfprintf(stderr, fmt, ap);
 		fprintf(stderr, "\n");
@@ -1233,12 +1235,12 @@ verifyemptynodes(const vctx_t *vctx, const dns_name_t *name,
 }

 static void
-vctx_init(vctx_t *vctx, isc_mem_t *mctx, dns_zone_t *zone, dns_db_t *db,
+vctx_init(vctx_t *vctx, isc_mem_t *mctx, const char *zonename, dns_db_t *db,
 	  dns_dbversion_t *ver, dns_name_t *origin, dns_keytable_t *secroots) {
 	memset(vctx, 0, sizeof(*vctx));

 	vctx->mctx = mctx;
-	vctx->zone = zone;
+	vctx->zonename = zonename;
 	vctx->db = db;
 	vctx->ver = ver;
 	vctx->origin = origin;
@@ -1908,7 +1910,7 @@ print_summary(const vctx_t *vctx, bool keyset_kskonly,
 }

 isc_result_t
-dns_zoneverify_dnssec(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver,
+dns_zoneverify_dnssec(const char *zonename, dns_db_t *db, dns_dbversion_t *ver,
 		      dns_name_t *origin, dns_keytable_t *secroots,
 		      isc_mem_t *mctx, bool ignore_kskflag, bool keyset_kskonly,
 		      void (*report)(const char *, ...)) {
@@ -1916,7 +1918,7 @@ dns_zoneverify_dnssec(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver,
 	isc_result_t result, vresult = ISC_R_UNSET;
 	vctx_t vctx;

-	vctx_init(&vctx, mctx, zone, db, ver, origin, secroots);
+	vctx_init(&vctx, mctx, zonename, db, ver, origin, secroots);

 	result = check_apex_rrsets(&vctx);
 	if (result != ISC_R_SUCCESS) {
diff --git a/tests/dns/meson.build b/tests/dns/meson.build
index 5d407d7497..bb173447a8 100644
--- a/tests/dns/meson.build
+++ b/tests/dns/meson.build
@@ -52,8 +52,10 @@ dns_tests = [
     'unreachcache',
     'update',
     'vecheader',
+    'viewname',
     'zonefile',
     'zonemgr',
+    'zoneverify',
     'zt',
 ]

diff --git a/tests/dns/viewname_test.c b/tests/dns/viewname_test.c
new file mode 100644
index 0000000000..da1913d03b
--- /dev/null
+++ b/tests/dns/viewname_test.c
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <inttypes.h>
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/lib.h>
+#include <isc/log.h>
+
+#include <dns/lib.h>
+#include <dns/view.h>
+#include <dns/zone.h>
+#include <dns/zoneproperties.h>
+
+#include "zone_p.h"
+
+#include <tests/dns.h>
+
+static int
+setup_test(void **state) {
+	setup_loopmgr(state);
+	return 0;
+}
+
+static int
+teardown_test(void **state) {
+	teardown_loopmgr(state);
+	return 0;
+}
+
+ISC_LOOP_TEST_IMPL(lifetime) {
+	const char *names[] = { "_default", "_bind", "internal" };
+	const char *display[] = { "example/IN", "example/IN",
+				  "example/IN/internal" };
+	FILE *log = tmpfile();
+	assert_non_null(log);
+	isc_log_createandusechannel(
+		isc_logconfig_get(), "viewname", ISC_LOG_TOFILEDESC,
+		ISC_LOG_INFO, ISC_LOGDESTINATION_FILE(log), 0,
+		ISC_LOGCATEGORY_DEFAULT, ISC_LOGMODULE_DEFAULT);
+
+	for (size_t i = 0; i < ARRAY_SIZE(names); i++) {
+		dns_zone_t *zone = NULL, *held = NULL;
+		dns_view_t *view = NULL, *replacement = NULL;
+		char buf[256], expected[256];
+		isc_buffer_t b;
+
+		assert_int_equal(
+			dns_test_makezone("example", &zone, NULL, false),
+			ISC_R_SUCCESS);
+		assert_int_equal(
+			dns_test_makeview(names[i], false, false, &view),
+			ISC_R_SUCCESS);
+		dns_zone_setview(zone, view);
+		assert_string_equal(dns__viewname_get(&zone->viewname),
+				    names[i]);
+		dns_zone_name(zone, buf, sizeof(buf));
+		assert_string_equal(buf, display[i]);
+
+		/* Reconfiguration with the same name preserves the allocation.
+		 */
+		const char *saved = dns__viewname_get(&zone->viewname);
+		assert_int_equal(
+			dns_test_makeview(names[i], false, false, &replacement),
+			ISC_R_SUCCESS);
+		dns_zone_setview(zone, replacement);
+		assert_ptr_equal(saved, dns__viewname_get(&zone->viewname));
+		dns_zone_setviewrevert(zone);
+		assert_ptr_equal(saved, dns__viewname_get(&zone->viewname));
+		dns_view_detach(&replacement);
+
+		/* A changed name and rollback both update the cached value. */
+		assert_int_equal(
+			dns_test_makeview("other", false, false, &replacement),
+			ISC_R_SUCCESS);
+		dns_zone_setview(zone, replacement);
+		dns_zone_name(zone, buf, sizeof(buf));
+		assert_string_equal(buf, "example/IN/other");
+		dns_zone_setviewrevert(zone);
+		dns_zone_name(zone, buf, sizeof(buf));
+		assert_string_equal(buf, display[i]);
+		dns_view_detach(&replacement);
+
+		/* Full names, including built-ins, still expand in filenames.
+		 */
+		isc_buffer_init(&b, buf, sizeof(buf));
+		dns_zone_expandzonefile(
+			&b, "$view/$name.db", dns_zone_getorigin(zone),
+			dns__viewname_get(&zone->viewname), "primary");
+		snprintf(expected, sizeof(expected), "%s/example.db", names[i]);
+		assert_string_equal(buf, expected);
+
+		/* An internal reference keeps the zone, but not its view,
+		 * alive. */
+		dns_zone_iattach(zone, &held);
+		dns_zone_detach(&zone);
+		assert_null(dns_zone_getview(held));
+		dns_view_detach(&view);
+		dns_zone_name(held, buf, sizeof(buf));
+		assert_string_equal(buf, display[i]);
+		dns_zone_log(held, ISC_LOG_INFO, "after shutdown");
+		dns_zone_idetach(&held);
+	}
+
+	char output[4096];
+	assert_int_equal(fflush(log), 0);
+	assert_int_equal(fseek(log, 0, SEEK_SET), 0);
+	size_t length = fread(output, 1, sizeof(output) - 1, log);
+	assert_false(ferror(log));
+	output[length] = '\0';
+	assert_non_null(strstr(output, "zone example/IN: after shutdown"));
+	assert_non_null(
+		strstr(output, "zone example/IN/internal: after shutdown"));
+	assert_null(strstr(output, "_default"));
+	assert_null(strstr(output, "_bind"));
+	isc_log_createandusechannel(
+		isc_logconfig_get(), "viewname", ISC_LOG_TONULL, ISC_LOG_INFO,
+		NULL, 0, ISC_LOGCATEGORY_DEFAULT, ISC_LOGMODULE_DEFAULT);
+	fclose(log);
+	isc_loopmgr_shutdown();
+}
+
+ISC_TEST_LIST_START
+ISC_TEST_ENTRY_CUSTOM(lifetime, setup_test, teardown_test)
+ISC_TEST_LIST_END
+
+ISC_TEST_MAIN
diff --git a/tests/dns/zoneverify_test.c b/tests/dns/zoneverify_test.c
new file mode 100644
index 0000000000..73d3baa182
--- /dev/null
+++ b/tests/dns/zoneverify_test.c
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <inttypes.h>
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/lib.h>
+#include <isc/log.h>
+#include <isc/work.h>
+
+#include <dns/db.h>
+#include <dns/lib.h>
+#include <dns/result.h>
+#include <dns/view.h>
+#include <dns/zone.h>
+#include <dns/zoneproperties.h>
+
+#include <tests/dns.h>
+
+static int
+setup_test(void **state) {
+	setup_loopmgr(state);
+	return 0;
+}
+
+static int
+teardown_test(void **state) {
+	teardown_loopmgr(state);
+	return 0;
+}
+
+typedef struct {
+	dns_view_t *view;
+	dns_db_t *db;
+	FILE *log;
+} verify_test_t;
+
+static isc_result_t
+verify_worker(void *arg) {
+	verify_test_t *test = arg;
+	dns_dbversion_t *version = NULL;
+	isc_result_t result;
+
+	/* Exercise both current-version and supplied-version ownership. */
+	result = dns_zone_verifydb(test->view, test->db, NULL);
+	if (result != DNS_R_VERIFYFAILURE) {
+		return ISC_R_FAILURE;
+	}
+	RETERR(dns_db_newversion(test->db, &version));
+	result = dns_zone_verifydb(test->view, test->db, version);
+	dns_db_closeversion(test->db, &version, false);
+	return result;
+}
+
+static void
+verify_done(void *arg, isc_result_t result) {
+	verify_test_t *test = arg;
+	char output[4096];
+	size_t length;
+
+	assert_int_equal(result, DNS_R_VERIFYFAILURE);
+	assert_int_equal(fflush(test->log), 0);
+	assert_int_equal(fseek(test->log, 0, SEEK_SET), 0);
+	length = fread(output, 1, sizeof(output) - 1, test->log);
+	assert_false(ferror(test->log));
+	output[length] = '\0';
+	assert_non_null(strstr(output, "zone example/IN/worker-view: Zone "
+				       "contains no DNSSEC keys"));
+	assert_non_null(strstr(output, "zone example/IN/worker-view: zone "
+				       "verification failed:"));
+
+	/* Stop using the temporary stream before closing it. */
+	isc_log_createandusechannel(isc_logconfig_get(), "verification",
+				    ISC_LOG_TONULL, ISC_LOG_INFO, NULL, 0,
+				    ISC_LOGCATEGORY_DEFAULT,
+				    ISC_LOGMODULE_DEFAULT);
+	fclose(test->log);
+	dns_view_weakdetach(&test->view);
+	dns_db_detach(&test->db);
+	isc_loopmgr_shutdown();
+}
+
+ISC_LOOP_TEST_IMPL(after_shutdown) {
+	static verify_test_t test;
+	dns_zone_t *zone = NULL;
+	dns_view_t *view = NULL;
+	UNUSED(arg);
+
+	assert_int_equal(dns_test_makezone("example", &zone, NULL, false),
+			 ISC_R_SUCCESS);
+	assert_int_equal(dns_test_makeview("worker-view", false, false, &view),
+			 ISC_R_SUCCESS);
+	dns_zone_setview(zone, view);
+	dns_view_initsecroots(view);
+	/* Keep the same weak view reference that a transfer owns. */
+	dns_view_weakattach(view, &test.view);
+	assert_int_equal(dns_db_create(isc_g_mctx, ZONEDB_DEFAULT,
+				       dns_zone_getorigin(zone),
+				       dns_dbtype_zone, dns_rdataclass_in, 0,
+				       NULL, &test.db),
+			 ISC_R_SUCCESS);
+
+	/* The unmanaged zone shuts down synchronously on its final detach. */
+	dns_zone_detach(&zone);
+	dns_view_detach(&view);
+
+	test.log = tmpfile();
+	assert_non_null(test.log);
+	isc_log_createandusechannel(
+		isc_logconfig_get(), "verification", ISC_LOG_TOFILEDESC,
+		ISC_LOG_INFO, ISC_LOGDESTINATION_FILE(test.log), 0,
+		ISC_LOGCATEGORY_DEFAULT, ISC_LOGMODULE_DEFAULT);
+
+	isc_work_enqueue(isc_loop(), ISC_WORKLANE_SLOW, verify_worker,
+			 verify_done, &test);
+}
+
+ISC_TEST_LIST_START
+ISC_TEST_ENTRY_CUSTOM(after_shutdown, setup_test, teardown_test)
+ISC_TEST_LIST_END
+
+ISC_TEST_MAIN