From 4f180e095a4283dbf25fc512ce5b9a3d0b273b0e Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Thu, 5 Feb 2026 15:48:03 -0800 Subject: [PATCH] Fix activating large_microzap on receive This ensures that the in-memory state of the feature is recorded and that `dsl_dataset_activate_feature` is not called when the feature is already active. Reviewed-by: Alexander Motin Reviewed-by: Brian Behlendorf Signed-off-by: Austin Wise Closes #18143 Closes #18144 --- module/zfs/dmu_recv.c | 38 +++---- tests/runfiles/common.run | 1 + tests/zfs-tests/include/tunables.cfg | 1 + tests/zfs-tests/tests/Makefile.am | 2 + .../rsend/send_large_microzap_incremental.ksh | 91 ++++++++++++++++ .../rsend/send_large_microzap_transitive.ksh | 100 ++++++++++++++++++ 6 files changed, 215 insertions(+), 18 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/rsend/send_large_microzap_incremental.ksh create mode 100755 tests/zfs-tests/tests/functional/rsend/send_large_microzap_transitive.ksh diff --git a/module/zfs/dmu_recv.c b/module/zfs/dmu_recv.c index 66ec6c1b0..fa18a2056 100644 --- a/module/zfs/dmu_recv.c +++ b/module/zfs/dmu_recv.c @@ -997,24 +997,6 @@ dmu_recv_begin_sync(void *arg, dmu_tx_t *tx) numredactsnaps, tx); } - if (featureflags & DMU_BACKUP_FEATURE_LARGE_MICROZAP) { - /* - * The source has seen a large microzap at least once in its - * life, so we activate the feature here to match. It's not - * strictly necessary since a large microzap is usable without - * the feature active, but if that object is sent on from here, - * we need this info to know to add the stream feature. - * - * There may be no large microzap in the incoming stream, or - * ever again, but this is a very niche feature and its very - * difficult to spot a large microzap in the stream, so its - * not worth the effort of trying harder to activate the - * feature at first use. - */ - dsl_dataset_activate_feature(dsobj, SPA_FEATURE_LARGE_MICROZAP, - (void *)B_TRUE, tx); - } - dmu_buf_will_dirty(newds->ds_dbuf, tx); dsl_dataset_phys(newds)->ds_flags |= DS_FLAG_INCONSISTENT; @@ -1044,6 +1026,26 @@ dmu_recv_begin_sync(void *arg, dmu_tx_t *tx) newds->ds_feature[SPA_FEATURE_LONGNAME] = (void *)B_TRUE; } + if (featureflags & DMU_BACKUP_FEATURE_LARGE_MICROZAP && + !dsl_dataset_feature_is_active(newds, SPA_FEATURE_LARGE_MICROZAP)) { + /* + * The source has seen a large microzap at least once in its + * life, so we activate the feature here to match. It's not + * strictly necessary since a large microzap is usable without + * the feature active, but if that object is sent on from here, + * we need this info to know to add the stream feature. + * + * There may be no large microzap in the incoming stream, or + * ever again, but this is a very niche feature and its very + * difficult to spot a large microzap in the stream, so its + * not worth the effort of trying harder to activate the + * feature at first use. + */ + dsl_dataset_activate_feature(dsobj, SPA_FEATURE_LARGE_MICROZAP, + (void *)B_TRUE, tx); + newds->ds_feature[SPA_FEATURE_LARGE_MICROZAP] = (void *)B_TRUE; + } + /* * If we actually created a non-clone, we need to create the objset * in our new dataset. If this is a raw send we postpone this until diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index fc0ae4300..b83f50a4b 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -998,6 +998,7 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos', 'send_spill_block', 'send_holds', 'send_hole_birth', 'send_mixed_raw', 'send-wR_encrypted_zvol', 'send_partial_dataset', 'send_invalid', 'send_large_blocks_incremental', 'send_large_blocks_initial', + 'send_large_microzap_incremental', 'send_large_microzap_transitive', 'send_doall', 'send_raw_spill_block', 'send_raw_ashift', 'send_raw_large_blocks', 'send_leak_keymaps'] tags = ['functional', 'rsend'] diff --git a/tests/zfs-tests/include/tunables.cfg b/tests/zfs-tests/include/tunables.cfg index 36621b218..e75d00e1b 100644 --- a/tests/zfs-tests/include/tunables.cfg +++ b/tests/zfs-tests/include/tunables.cfg @@ -114,6 +114,7 @@ BCLONE_WAIT_DIRTY bclone_wait_dirty zfs_bclone_wait_dirty DIO_ENABLED dio_enabled zfs_dio_enabled DIO_STRICT dio_strict zfs_dio_strict XATTR_COMPAT xattr_compat zfs_xattr_compat +ZAP_MICRO_MAX_SIZE zap_micro_max_size zap_micro_max_size ZEVENT_LEN_MAX zevent.len_max zfs_zevent_len_max ZEVENT_RETAIN_MAX zevent.retain_max zfs_zevent_retain_max ZIO_SLOW_IO_MS zio.slow_io_ms zio_slow_io_ms diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 7d4d53b2c..a9e08ae30 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -2067,6 +2067,8 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/rsend/send_invalid.ksh \ functional/rsend/send_large_blocks_incremental.ksh \ functional/rsend/send_large_blocks_initial.ksh \ + functional/rsend/send_large_microzap_incremental.ksh \ + functional/rsend/send_large_microzap_transitive.ksh \ functional/rsend/send_leak_keymaps.ksh \ functional/rsend/send-L_toggle.ksh \ functional/rsend/send_mixed_raw.ksh \ diff --git a/tests/zfs-tests/tests/functional/rsend/send_large_microzap_incremental.ksh b/tests/zfs-tests/tests/functional/rsend/send_large_microzap_incremental.ksh new file mode 100755 index 000000000..ca76aca1f --- /dev/null +++ b/tests/zfs-tests/tests/functional/rsend/send_large_microzap_incremental.ksh @@ -0,0 +1,91 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 + +# +# 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. +# + +# +# Copyright (c) 2026 by Austin Wise. All rights reserved. +# + +. $STF_SUITE/tests/functional/rsend/rsend.kshlib +. $STF_SUITE/include/properties.shlib + +# +# Description: +# Ensure that it is possible to receive an incremental send of a snapshot that has the large microzap +# feature active into a pool where the feature is already active. +# Regression test for https://github.com/openzfs/zfs/issues/18143 +# +# Strategy: +# 1. Activate the large microzap feature in the source dataset. +# 2. Create a snapshot. +# 3. Send the snapshot from the first pool to a second pool. +# 4. Create a second snapshot. +# 5. Send the second snapshot incrementally to the second pool. +# + +verify_runnable "both" + +log_assert "Verify incremental receive handles inactive large_blocks feature correctly." + +function cleanup +{ + restore_tunable ZAP_MICRO_MAX_SIZE + cleanup_pool $POOL + cleanup_pool $POOL2 +} + +function assert_feature_state { + typeset pool=$1 + typeset expected_state=$2 + + typeset actual_state=$(zpool get -H -o value feature@large_microzap $pool) + log_note "Zpool $pool feature@large_microzap=$actual_state" + if [[ "$actual_state" != "$expected_state" ]]; then + log_fail "pool $pool feature@large_microzap=$actual_state (expected '$expected_state')" + fi +} + +typeset src=$POOL/src +typeset second=$POOL2/second + +log_onexit cleanup + +# Allow micro ZAPs to grow beyond SPA_OLD_MAXBLOCKSIZE. +set_tunable64 ZAP_MICRO_MAX_SIZE 1048576 + +# Create a dataset with a large recordsize (1MB) +log_must zfs create -o recordsize=1M $src +typeset mntpnt=$(get_prop mountpoint $src) + +# Activate the large_microzap feature by creating a micro ZAP that is larger than SPA_OLD_MAXBLOCKSIZE (128k) +# but smaller than MZAP_MAX_SIZE (1MB). Each micro ZAP entry is 64 bytes (MZAP_ENT_LEN), +# so 4096 files is about 256k. +log_must eval "seq 1 4096 | xargs -I REPLACE_ME touch $mntpnt/REPLACE_ME" +log_must zpool sync $POOL + +# Assert initial state of pools +assert_feature_state $POOL "active" +assert_feature_state $POOL2 "enabled" + +# Create initial snapshot and send to second pool. +log_must zfs snapshot $src@snap +log_must eval "zfs send -p -L $src@snap | zfs receive $second" +log_must zpool sync $POOL2 +assert_feature_state $POOL2 "active" + +# Create a second snapshot and send incrementally. +# This ensures that the feature is not activated a second time, which would cause a panic. +log_must zfs snapshot $src@snap2 +log_must eval "zfs send -L -i $src@snap $src@snap2 | zfs receive -F $second" + +log_pass "Feature activation propagated successfully." diff --git a/tests/zfs-tests/tests/functional/rsend/send_large_microzap_transitive.ksh b/tests/zfs-tests/tests/functional/rsend/send_large_microzap_transitive.ksh new file mode 100755 index 000000000..f44ba4966 --- /dev/null +++ b/tests/zfs-tests/tests/functional/rsend/send_large_microzap_transitive.ksh @@ -0,0 +1,100 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 + +# +# 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. +# + +# +# Copyright (c) 2026 by Austin Wise. All rights reserved. +# + +. $STF_SUITE/tests/functional/rsend/rsend.kshlib +. $STF_SUITE/include/properties.shlib + +# +# Description: +# Ensures that sending a snapshot with the large microzap feature active propagates +# the feature activation to the receiving pool. Also checks that the received snapshot also +# correctly propagates the feature activation when sent to a third third pool. +# Regression test for https://github.com/openzfs/zfs/issues/18143 +# +# Strategy: +# 1. Enable the large microzap feature in the source dataset. +# 2. Create a snapshot. +# 3. Send the snapshot from the first pool to a second pool. +# 4. Send the snapshot from the second pool to a third pool. +# 5. Verify that all pools have the large microzap feature active. +# + +verify_runnable "both" +verify_disk_count "$DISKS" 3 + +log_assert "Verify incremental receive handles inactive large_blocks feature correctly." + +function cleanup +{ + restore_tunable ZAP_MICRO_MAX_SIZE + cleanup_pool $POOL + cleanup_pool $POOL2 + cleanup_pool $POOL3 +} + +function assert_feature_state { + typeset pool=$1 + typeset expected_state=$2 + + typeset actual_state=$(zpool get -H -o value feature@large_microzap $pool) + log_note "Zpool $pool feature@large_microzap=$actual_state" + if [[ "$actual_state" != "$expected_state" ]]; then + log_fail "pool $pool feature@large_microzap=$actual_state (expected '$expected_state')" + fi +} + +typeset src=$POOL/src +typeset second=$POOL2/second +typeset third=$POOL3/third + +log_onexit cleanup + +# Allow micro ZAPs to grow beyond SPA_OLD_MAXBLOCKSIZE. +set_tunable64 ZAP_MICRO_MAX_SIZE 1048576 + +# Ensure the third pool exists. +datasetexists $POOL3 || log_must zpool create $POOL3 $DISK3 + +# Create a dataset with a large recordsize (1MB) +log_must zfs create -o recordsize=1M $src +typeset mntpnt=$(get_prop mountpoint $src) + +# Activate the large_microzap feature by creating a micro ZAP that is larger than SPA_OLD_MAXBLOCKSIZE (128k) +# but smaller than MZAP_MAX_SIZE (1MB). Each micro ZAP entry is 64 bytes (MZAP_ENT_LEN), +# so 4096 files is about 256k. +log_must eval "seq 1 4096 | xargs -I REPLACE_ME touch $mntpnt/REPLACE_ME" +log_must zpool sync $POOL + +# Assert initial state of pools +assert_feature_state $POOL "active" +assert_feature_state $POOL2 "enabled" +assert_feature_state $POOL3 "enabled" + +# Create initial snapshot and send to second pool. +log_must zfs snapshot $src@snap +log_must eval "zfs send -p -L $src@snap | zfs receive $second" +log_must zpool sync $POOL2 +assert_feature_state $POOL2 "active" + +# Send to third pool from second. This ensures that the second pool correctly recorded that that +# large_microzap feature was active when it received the first send stream. +log_must eval "zfs send -p -L $second@snap | zfs receive $third" +log_must zpool sync $POOL3 +assert_feature_state $POOL3 "active" + +log_pass "Feature activation propagated successfully."