aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHeonJe Lee <lhjnano@gmail.com>2026-07-27 22:00:39 +0000
committerTony Hutter <hutter2@llnl.gov>2026-08-13 16:40:04 +0000
commitcc010bbc34915310c6f31ca001a66f16b2ca3265 (patch)
treedd4b637f6d2f14e1188bd46429bf2131b6fe7fcd
parent8ff555ea8a4a319c2632e589172dda761d5f66a9 (diff)
zpool export: return EBUSY when zvol minors are in use
When a zvol block device is open (e.g., mounted by the OS), exporting the pool can hang indefinitely in zvol_remove_minors_impl() which calls cv_wait(&zv->zv_removing_cv) with no timeout. This is because the open holder never closes the device, so the condition variable is never signaled. Fix this by taking an additional spa reference in zvol_first_open() and releasing it in zvol_last_close(). With this reference, spa_export_common() will return EBUSY via the existing spa_refcount_zero() check, matching the behavior already provided for mounted ZFS datasets. Additionally, move the zvol_remove_minors() call in spa_export_common() to after the spa_refcount_zero() check. Previously it was called before the check, which could still hang if a zvol was open. Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Signed-off-by: Heonje LEE <lhjnano@gmail.com> Closes #18841
-rw-r--r--module/zfs/spa.c10
-rw-r--r--module/zfs/zvol.c11
-rw-r--r--tests/runfiles/common.run2
-rw-r--r--tests/zfs-tests/tests/Makefile.am1
-rwxr-xr-xtests/zfs-tests/tests/functional/cli_root/zpool_export/zpool_export_005_neg.ksh89
5 files changed, 108 insertions, 5 deletions
diff --git a/module/zfs/spa.c b/module/zfs/spa.c
index 1ad94fd1bf7f..333aea97bc29 100644
--- a/module/zfs/spa.c
+++ b/module/zfs/spa.c
@@ -7682,10 +7682,7 @@ spa_export_common(const char *pool, int new_state, nvlist_t **oldconfig,
spa_open_ref(spa, FTAG);
spa_namespace_exit(FTAG);
spa_async_suspend(spa);
- if (spa->spa_zvol_taskq) {
- zvol_remove_minors(spa, spa_name(spa), B_TRUE);
- taskq_wait(spa->spa_zvol_taskq);
- }
+
spa_namespace_enter(FTAG);
spa->spa_export_thread = curthread;
spa_close(spa, FTAG);
@@ -7723,6 +7720,11 @@ spa_export_common(const char *pool, int new_state, nvlist_t **oldconfig,
* that we are finshed.
*/
+ if (spa->spa_zvol_taskq) {
+ zvol_remove_minors(spa, spa_name(spa), B_TRUE);
+ taskq_wait(spa->spa_zvol_taskq);
+ }
+
if (spa->spa_sync_on) {
vdev_t *rvd = spa->spa_root_vdev;
/*
diff --git a/module/zfs/zvol.c b/module/zfs/zvol.c
index 15ee62ca5249..fbc3c34ea3a0 100644
--- a/module/zfs/zvol.c
+++ b/module/zfs/zvol.c
@@ -1262,6 +1262,13 @@ zvol_first_open(zvol_state_t *zv, boolean_t readonly)
if (error) {
dmu_objset_disown(os, 1, zv);
zv->zv_objset = NULL;
+ } else {
+ /*
+ * Take a hold on the spa so that spa_export_common() will
+ * return EBUSY while the zvol block device is open, just
+ * as it does for mounted datasets.
+ */
+ spa_open_ref(dmu_objset_spa(os), zv);
}
return (error);
@@ -1276,10 +1283,14 @@ zvol_last_close(zvol_state_t *zv)
if (zv->zv_flags & ZVOL_REMOVING)
cv_broadcast(&zv->zv_removing_cv);
+ spa_t *spa = dmu_objset_spa(zv->zv_objset);
+
zvol_shutdown_zv(zv);
dmu_objset_disown(zv->zv_objset, 1, zv);
zv->zv_objset = NULL;
+
+ spa_close(spa, zv);
}
typedef struct minors_job {
diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run
index d84c11feb73d..bf08e15dc71c 100644
--- a/tests/runfiles/common.run
+++ b/tests/runfiles/common.run
@@ -452,7 +452,7 @@ tags = ['functional', 'cli_root', 'zpool_events']
[tests/functional/cli_root/zpool_export]
tests = ['zpool_export_001_pos', 'zpool_export_002_pos',
'zpool_export_003_neg', 'zpool_export_004_pos',
- 'zpool_export_parallel_pos', 'zpool_export_parallel_admin']
+ 'zpool_export_005_neg', 'zpool_export_parallel_pos', 'zpool_export_parallel_admin']
tags = ['functional', 'cli_root', 'zpool_export']
[tests/functional/cli_root/zpool_get]
diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am
index 2d7b923851e6..ba396c1e9191 100644
--- a/tests/zfs-tests/tests/Makefile.am
+++ b/tests/zfs-tests/tests/Makefile.am
@@ -1121,6 +1121,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/cli_root/zpool_export/zpool_export_002_pos.ksh \
functional/cli_root/zpool_export/zpool_export_003_neg.ksh \
functional/cli_root/zpool_export/zpool_export_004_pos.ksh \
+ functional/cli_root/zpool_export/zpool_export_005_neg.ksh \
functional/cli_root/zpool_export/zpool_export_parallel_admin.ksh \
functional/cli_root/zpool_export/zpool_export_parallel_pos.ksh \
functional/cli_root/zpool_get/cleanup.ksh \
diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool_export/zpool_export_005_neg.ksh b/tests/zfs-tests/tests/functional/cli_root/zpool_export/zpool_export_005_neg.ksh
new file mode 100755
index 000000000000..2978f8c6a28d
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/cli_root/zpool_export/zpool_export_005_neg.ksh
@@ -0,0 +1,89 @@
+#!/bin/ksh -p
+# SPDX-License-Identifier: CDDL-1.0
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or https://opensource.org/licenses/CDDL-1.0.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2025 by Heonje LEE. All rights reserved.
+#
+
+. $STF_SUITE/include/libtest.shlib
+. $STF_SUITE/tests/functional/cli_root/zpool_export/zpool_export.kshlib
+
+#
+# DESCRIPTION:
+# Exporting a pool with a mounted zvol block device must fail with
+# EBUSY rather than hanging the system in an uninterruptible sleep.
+# This mirrors the behavior already provided for mounted ZFS
+# filesystems (see zpool_export_002_pos.ksh).
+#
+# STRATEGY:
+# 1. Create a pool with a zvol.
+# 2. Format and mount the zvol block device.
+# 3. Attempt to export the pool -- must fail (EBUSY).
+# 4. Unmount the zvol.
+# 5. Export should now succeed.
+#
+
+verify_runnable "global"
+
+function cleanup
+{
+ ismounted $mntpnt $NEWFS_DEFAULT_FS && log_must umount $mntpnt
+ [[ -d $mntpnt ]] && log_must rmdir $mntpnt
+ datasetexists $TESTPOOL1/vol && log_must zfs destroy $TESTPOOL1/vol
+ poolexists $TESTPOOL1 && destroy_pool $TESTPOOL1
+ [[ -f $vdev ]] && log_must rm -f $vdev
+ zpool_export_cleanup
+}
+
+mntpnt=$TESTDIR0/zvol_export_mnt
+vdev=$TESTDIR0/zvol_export_vdev
+
+log_onexit cleanup
+
+log_assert "Exporting a pool with a mounted zvol returns EBUSY"
+
+log_must mkdir -p $TESTDIR0
+log_must truncate -s $MINVDEVSIZE $vdev
+
+# 1. Create pool + zvol
+log_must zpool create $TESTPOOL1 $vdev
+log_must zfs create -V 100M $TESTPOOL1/vol
+block_device_wait ${ZVOL_DEVDIR}/$TESTPOOL1/vol
+
+# 2. Format and mount the zvol block device
+log_must eval "new_fs ${ZVOL_RDEVDIR}/$TESTPOOL1/vol >/dev/null 2>&1"
+log_must mkdir -p $mntpnt
+log_must mount ${ZVOL_DEVDIR}/$TESTPOOL1/vol $mntpnt
+
+# 3. Export must fail with EBUSY (not hang)
+log_mustnot zpool export $TESTPOOL1
+log_must poolexists $TESTPOOL1
+
+# 4. Unmount the zvol
+log_must umount $mntpnt
+
+# 5. Export should now succeed
+log_must zpool export $TESTPOOL1
+log_mustnot poolexists $TESTPOOL1
+
+log_pass "Export with mounted zvol correctly returned EBUSY"