diff --git a/module/zfs/vdev_removal.c b/module/zfs/vdev_removal.c index 89911e55b..526ef680b 100644 --- a/module/zfs/vdev_removal.c +++ b/module/zfs/vdev_removal.c @@ -2151,7 +2151,6 @@ spa_vdev_remove_log(vdev_t *vd, uint64_t *txg) ASSERT0P(vd->vdev_log_mg); return (error); } - ASSERT0(vd->vdev_stat.vs_alloc); /* * The evacuation succeeded. Remove any remaining MOS metadata diff --git a/module/zfs/zil.c b/module/zfs/zil.c index 0307df55a..0fa58d5cc 100644 --- a/module/zfs/zil.c +++ b/module/zfs/zil.c @@ -1096,7 +1096,7 @@ zil_destroy(zilog_t *zilog, boolean_t keep_first) zilog->zl_old_header = *zh; /* debugging aid */ - if (BP_IS_HOLE(&zh->zh_log)) + if (BP_IS_HOLE(&zh->zh_log) && zh->zh_flags == 0) return (B_FALSE); tx = dmu_tx_create(zilog->zl_os); @@ -1166,6 +1166,15 @@ zil_claim(dsl_pool_t *dp, dsl_dataset_t *ds, void *txarg) zilog = dmu_objset_zil(os); zh = zil_header_in_syncing_context(zilog); ASSERT3U(tx->tx_txg, ==, spa_first_txg(zilog->zl_spa)); + + /* + * If the log is empty, then there is nothing to do here. + */ + if (BP_IS_HOLE(&zh->zh_log)) { + dmu_objset_disown(os, B_FALSE, FTAG); + return (0); + } + first_txg = spa_min_claim_txg(zilog->zl_spa); /* @@ -1198,11 +1207,14 @@ zil_claim(dsl_pool_t *dp, dsl_dataset_t *ds, void *txarg) if (spa_get_log_state(zilog->zl_spa) == SPA_LOG_CLEAR || (zilog->zl_spa->spa_uberblock.ub_checkpoint_txg != 0 && zh->zh_claim_txg == 0)) { - if (!BP_IS_HOLE(&zh->zh_log)) { + if (zilog->zl_spa->spa_uberblock.ub_checkpoint_txg != 0 && + BP_GET_BIRTH(&zh->zh_log) < first_txg) { (void) zil_parse(zilog, zil_clear_log_block, zil_noop_log_record, tx, first_txg, B_FALSE); + } else { + zio_free(zilog->zl_spa, first_txg, &zh->zh_log); } - BP_ZERO(&zh->zh_log); + memset(zh, 0, sizeof (zil_header_t)); if (os->os_encrypted) os->os_next_write_raw[tx->tx_txg & TXG_MASK] = B_TRUE; dsl_dataset_dirty(dmu_objset_ds(os), tx); @@ -1224,7 +1236,7 @@ zil_claim(dsl_pool_t *dp, dsl_dataset_t *ds, void *txarg) * or destroy beyond the last block we successfully claimed. */ ASSERT3U(zh->zh_claim_txg, <=, first_txg); - if (zh->zh_claim_txg == 0 && !BP_IS_HOLE(&zh->zh_log)) { + if (zh->zh_claim_txg == 0) { (void) zil_parse(zilog, zil_claim_log_block, zil_claim_log_record, tx, first_txg, B_FALSE); zh->zh_claim_txg = first_txg; diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 16d16acf7..0266f22e4 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -947,7 +947,7 @@ tests = ['removal_all_vdev', 'removal_cancel', 'removal_check_space', 'removal_with_write', 'removal_with_zdb', 'remove_expanded', 'remove_mirror', 'remove_mirror_sanity', 'remove_raidz', 'remove_indirect', 'remove_attach_mirror', 'removal_reservation', - 'removal_with_hole'] + 'removal_with_hole', 'removal_with_missing_log'] tags = ['functional', 'removal'] [tests/functional/rename_dirs] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index fd1a66156..23c2d9e4b 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -1947,6 +1947,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/removal/removal_with_faulted.ksh \ functional/removal/removal_with_ganging.ksh \ functional/removal/removal_with_hole.ksh \ + functional/removal/removal_with_missing_log.ksh \ functional/removal/removal_with_indirect.ksh \ functional/removal/removal_with_remove.ksh \ functional/removal/removal_with_scrub.ksh \ diff --git a/tests/zfs-tests/tests/functional/removal/removal_with_missing_log.ksh b/tests/zfs-tests/tests/functional/removal/removal_with_missing_log.ksh new file mode 100755 index 000000000..ddf8a4505 --- /dev/null +++ b/tests/zfs-tests/tests/functional/removal/removal_with_missing_log.ksh @@ -0,0 +1,95 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# +# CDDL HEADER END +# + +# +# Copyright (c) 2026, TrueNAS. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/removal/removal.kshlib + +# +# DESCRIPTION: +# Verify that a missing SLOG device can be removed even when +# ZIL blocks exist on it. +# +# STRATEGY: +# 1. Create a pool with a SLOG device +# 2. Freeze the pool and write data to ZIL +# 3. Export the pool (ZIL blocks remain uncommitted) +# 4. Import with -N to claim logs without replay +# 5. Export and clear SLOG device labels to simulate failure +# 6. Import with -m (missing devices allowed) +# 7. Remove the missing SLOG vdev +# 8. Verify pool is healthy and space accounting is correct +# + +verify_runnable "global" + +log_assert "Removal of missing SLOG with ZIL blocks succeeds" + +function cleanup +{ + poolexists $TESTPOOL && destroy_pool $TESTPOOL +} + +log_onexit cleanup + +VDEV1="$(echo $DISKS | cut -d' ' -f1)" +VDEV2="$(echo $DISKS | cut -d' ' -f2)" + +# Create pool with SLOG and dataset +log_must zpool create $TESTPOOL $VDEV1 log $VDEV2 +log_must zfs create $TESTPOOL/$TESTFS + +# Create initial ZIL header (required before freezing) +log_must dd if=/dev/zero of=/$TESTPOOL/$TESTFS/init \ + conv=fdatasync,fsync bs=1 count=1 + +# Freeze pool and write data to ZIL +log_must zpool freeze $TESTPOOL +log_must dd if=/dev/urandom of=/$TESTPOOL/$TESTFS/file1 \ + oflag=sync bs=128k count=128 + +# Export with uncommitted ZIL transactions +log_must zpool export $TESTPOOL + +# Import with -N to claim logs without mounting/replaying +log_must zpool import -N $TESTPOOL +log_must zpool export $TESTPOOL + +# Clear SLOG labels to simulate device failure +log_must zpool labelclear -f $VDEV2 + +# Import with missing SLOG allowed +log_must zpool import -m $TESTPOOL +log_must eval "zpool status $TESTPOOL | grep UNAVAIL" + +# Remove the missing SLOG - should succeed +log_must zpool remove $TESTPOOL $VDEV2 +log_must zpool wait -t remove $TESTPOOL +sync_pool $TESTPOOL +log_mustnot eval "zpool status -v $TESTPOOL | grep $VDEV2" + +# Verify pool health +log_must zpool scrub -w $TESTPOOL +log_must check_pool_status $TESTPOOL "errors" "No known data errors" + +# Verify space accounting is correct +log_must zdb -c $TESTPOOL + +log_pass "Removal of missing SLOG with ZIL blocks succeeded"