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 <[email protected]>
Signed-off-by: Heonje LEE <[email protected]>
Closes #18841
This commit is contained in:
HeonJe Lee authored and GitHub committed 2026-07-27 15:00:39 -07:00
1 parent 67bb391670
commit 0bb41751f2
5 files changed
+108 -5

No files matched your search

+6 -4
View File
@@ -7761,10 +7761,7 @@ spa_export_common(const char *pool, int new_state, nvlist_t **oldconfig,
spa_condense_debug_cancel(spa);
#endif
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);
@@ -7802,6 +7799,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;
/*
+11
View File
@@ -1309,6 +1309,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);
@@ -1323,10 +1330,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 {
+1 -1
View File
@@ -463,7 +463,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]
+1
View File
@@ -1191,6 +1191,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 \
@@ -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"