diff options
| author | HeonJe Lee <lhjnano@gmail.com> | 2026-07-27 22:00:39 +0000 |
|---|---|---|
| committer | Tony Hutter <hutter2@llnl.gov> | 2026-08-13 16:40:04 +0000 |
| commit | cc010bbc34915310c6f31ca001a66f16b2ca3265 (patch) | |
| tree | dd4b637f6d2f14e1188bd46429bf2131b6fe7fcd | |
| parent | 8ff555ea8a4a319c2632e589172dda761d5f66a9 (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.c | 10 | ||||
| -rw-r--r-- | module/zfs/zvol.c | 11 | ||||
| -rw-r--r-- | tests/runfiles/common.run | 2 | ||||
| -rw-r--r-- | tests/zfs-tests/tests/Makefile.am | 1 | ||||
| -rwxr-xr-x | tests/zfs-tests/tests/functional/cli_root/zpool_export/zpool_export_005_neg.ksh | 89 |
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" |
