From aa29455dd720077b90c799838479d3630713cb3e Mon Sep 17 00:00:00 2001 From: Alexander Motin Date: Tue, 10 Feb 2026 12:53:24 -0500 Subject: [PATCH] Restrict cloning with different properties While technically its not a problem to clone between datasets with different properties, it might create expectation of new properties being applied during data move, while actually it won't happen. For copies and checksum it may mean incorrect safety expectations. For dedup, compression and special_small_blocks -- performance and space usage. New zfs_bclone_strict_properties tunable controls it. Reviewed-by: Rob Norris Reviewed-by: Brian Behlendorf Signed-off-by: Alexander Motin Closes #18180 --- man/man4/zfs.4 | 4 ++ module/zfs/zfs_vnops.c | 44 +++++++++++++++++++ tests/zfs-tests/include/tunables.cfg | 1 + .../bclone/bclone_crossfs_corner_cases.ksh | 9 ++++ .../bclone_crossfs_corner_cases_limited.ksh | 9 ++++ .../functional/bclone/bclone_crossfs_data.ksh | 7 +++ .../bclone/bclone_crossfs_embedded.ksh | 7 +++ .../bclone/bclone_diffprops_all.ksh | 28 ++++++++---- .../bclone/bclone_diffprops_checksum.ksh | 18 ++++++-- .../bclone/bclone_diffprops_compress.ksh | 16 +++++-- .../bclone/bclone_diffprops_copies.ksh | 18 ++++++-- .../bclone/bclone_diffprops_recordsize.ksh | 18 ++++++-- .../functional/bclone/bclone_prop_sync.ksh | 12 +++-- .../bclone/bclone_samefs_corner_cases.ksh | 7 +++ .../bclone_samefs_corner_cases_limited.ksh | 7 +++ .../functional/bclone/bclone_samefs_data.ksh | 6 +++ .../bclone/bclone_samefs_embedded.ksh | 6 +++ 17 files changed, 193 insertions(+), 24 deletions(-) diff --git a/man/man4/zfs.4 b/man/man4/zfs.4 index f87fe9cc4..97c0ac6ab 100644 --- a/man/man4/zfs.4 +++ b/man/man4/zfs.4 @@ -1448,6 +1448,10 @@ If this setting is 0, then even if feature@block_cloning is enabled, using functions and system calls that attempt to clone blocks will act as though the feature is disabled. . +.It Sy zfs_bclone_strict_properties Ns = Ns Sy 1 Ns | Ns 0 Pq int +Restricts block cloning between datasets with different properties +(checksum, compression, copies, dedup, or special_small_blocks). +. .It Sy zfs_bclone_wait_dirty Ns = Ns Sy 1 Ns | Ns 0 Pq int When set to 1 the FICLONE and FICLONERANGE ioctls will wait for any dirty data to be written to disk before proceeding. diff --git a/module/zfs/zfs_vnops.c b/module/zfs/zfs_vnops.c index 8f29474ac..1ceedf28e 100644 --- a/module/zfs/zfs_vnops.c +++ b/module/zfs/zfs_vnops.c @@ -69,6 +69,12 @@ */ int zfs_bclone_enabled = 1; +/* + * Restricts block cloning between datasets with different properties + * (checksum, compression, copies, dedup, or special_small_blocks). + */ +int zfs_bclone_strict_properties = 1; + /* * When set to 1 the FICLONE and FICLONERANGE ioctls will wait for any dirty * data to be written to disk before proceeding. This ensures that the clone @@ -1677,6 +1683,21 @@ zfs_clone_range(znode_t *inzp, uint64_t *inoffp, znode_t *outzp, return (SET_ERROR(EXDEV)); } + /* + * Cloning between datasets with different properties is possible, + * but it may cause confusions when copying data between them and + * expecting new properties to apply. + */ + if (zfs_bclone_strict_properties && inos != outos && + !inzfsvfs->z_issnap && + (inos->os_checksum != outos->os_checksum || + inos->os_compress != outos->os_compress || + inos->os_copies != outos->os_copies || + inos->os_dedup_checksum != outos->os_dedup_checksum)) { + zfs_exit_two(inzfsvfs, outzfsvfs, FTAG); + return (SET_ERROR(EXDEV)); + } + error = zfs_verify_zp(inzp); if (error == 0) error = zfs_verify_zp(outzp); @@ -1758,6 +1779,26 @@ zfs_clone_range(znode_t *inzp, uint64_t *inoffp, znode_t *outzp, inblksz = inzp->z_blksz; + /* + * Cloning between datasets with different special_small_blocks would + * bypass storage tier migration that would occur with a regular copy. + */ + if (zfs_bclone_strict_properties && inos != outos && + !inzfsvfs->z_issnap && spa_has_special(dmu_objset_spa(inos))) { + uint64_t in_smallblk = inos->os_zpl_special_smallblock; + uint64_t out_smallblk = outos->os_zpl_special_smallblock; + if (in_smallblk != out_smallblk) { + uint64_t min_smallblk = MIN(in_smallblk, out_smallblk); + uint64_t max_smallblk = MAX(in_smallblk, out_smallblk); + if (min_smallblk < inblksz && + (inos->os_compress != ZIO_COMPRESS_OFF || + max_smallblk >= inblksz)) { + error = SET_ERROR(EXDEV); + goto unlock; + } + } + } + /* * We cannot clone into a file with different block size if we can't * grow it (block size is already bigger, has more than one block, or @@ -2116,6 +2157,9 @@ ZFS_MODULE_PARAM(zfs_vnops, zfs_vnops_, read_chunk_size, U64, ZMOD_RW, ZFS_MODULE_PARAM(zfs, zfs_, bclone_enabled, INT, ZMOD_RW, "Enable block cloning"); +ZFS_MODULE_PARAM(zfs, zfs_, bclone_strict_properties, INT, ZMOD_RW, + "Restrict cross-dataset cloning with different properties"); + ZFS_MODULE_PARAM(zfs, zfs_, bclone_wait_dirty, INT, ZMOD_RW, "Wait for dirty blocks when cloning"); diff --git a/tests/zfs-tests/include/tunables.cfg b/tests/zfs-tests/include/tunables.cfg index e75d00e1b..5e6959a54 100644 --- a/tests/zfs-tests/include/tunables.cfg +++ b/tests/zfs-tests/include/tunables.cfg @@ -110,6 +110,7 @@ VOL_RECURSIVE vol.recursive UNSUPPORTED VOL_REQUEST_SYNC vol.request_sync zvol_request_sync VOL_USE_BLK_MQ UNSUPPORTED zvol_use_blk_mq BCLONE_ENABLED bclone_enabled zfs_bclone_enabled +BCLONE_STRICT_PROPERTIES bclone_strict_properties zfs_bclone_strict_properties BCLONE_WAIT_DIRTY bclone_wait_dirty zfs_bclone_wait_dirty DIO_ENABLED dio_enabled zfs_dio_enabled DIO_STRICT dio_strict zfs_dio_strict diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases.ksh index 31ca9acb2..01e9cf49d 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases.ksh @@ -32,6 +32,15 @@ verify_runnable "both" verify_crossfs_block_cloning +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS + log_must zfs inherit recordsize $TESTSRCFS + log_must zfs inherit recordsize $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify various corner cases in block cloning across datasets" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases_limited.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases_limited.ksh index 5a44ccd16..52f063c1c 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases_limited.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_corner_cases_limited.ksh @@ -32,6 +32,15 @@ verify_runnable "both" verify_crossfs_block_cloning +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS + log_must zfs inherit recordsize $TESTSRCFS + log_must zfs inherit recordsize $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify various corner cases in block cloning across datasets" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_data.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_data.ksh index 0ff4489f0..e1b583813 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_data.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_data.ksh @@ -32,6 +32,13 @@ verify_runnable "both" verify_crossfs_block_cloning +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning properly clones regular files across datasets" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_embedded.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_embedded.ksh index f68699858..b64a4533d 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_embedded.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_crossfs_embedded.ksh @@ -32,6 +32,13 @@ verify_runnable "both" verify_crossfs_block_cloning +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning properly clones small files (with embedded blocks) across datasets" # Enable ZLE compression to make sure what is the maximum amount of data we diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_all.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_all.ksh index bf67aaa0b..f6c19cf44 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_all.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_all.ksh @@ -33,8 +33,27 @@ verify_runnable "both" verify_crossfs_block_cloning +save_tunable BCLONE_STRICT_PROPERTIES + +function cleanup +{ + restore_tunable BCLONE_STRICT_PROPERTIES + log_must zfs inherit checksum $TESTSRCFS + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit copies $TESTSRCFS + log_must zfs inherit recordsize $TESTSRCFS + log_must zfs inherit checksum $TESTDSTFS + log_must zfs inherit compress $TESTDSTFS + log_must zfs inherit copies $TESTDSTFS + log_must zfs inherit recordsize $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning across datasets with different properties" +# Disable strict property checking to allow cross-dataset cloning with different properties +log_must set_tunable32 BCLONE_STRICT_PROPERTIES 0 + log_must zfs set checksum=off $TESTSRCFS log_must zfs set compress=off $TESTSRCFS log_must zfs set copies=1 $TESTSRCFS @@ -74,13 +93,4 @@ FILESIZE=$(random_int_between 2 32767) FILESIZE=$((FILESIZE * 64)) bclone_test text $FILESIZE false $TESTSRCDIR $TESTDSTDIR -log_must zfs inherit checksum $TESTSRCFS -log_must zfs inherit compress $TESTSRCFS -log_must zfs inherit copies $TESTSRCFS -log_must zfs inherit recordsize $TESTSRCFS -log_must zfs inherit checksum $TESTDSTFS -log_must zfs inherit compress $TESTDSTFS -log_must zfs inherit copies $TESTDSTFS -log_must zfs inherit recordsize $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_checksum.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_checksum.ksh index eacc66260..77821222e 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_checksum.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_checksum.ksh @@ -34,8 +34,23 @@ verify_runnable "both" verify_crossfs_block_cloning +save_tunable BCLONE_STRICT_PROPERTIES + +function cleanup +{ + restore_tunable BCLONE_STRICT_PROPERTIES + log_must zfs inherit checksum $TESTSRCFS + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit checksum $TESTDSTFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning across datasets with different checksum properties" +# Disable strict property checking to allow cross-dataset cloning with different properties +log_must set_tunable32 BCLONE_STRICT_PROPERTIES 0 + log_must zfs set compress=off $TESTSRCFS log_must zfs set compress=off $TESTDSTFS @@ -56,7 +71,4 @@ for srcprop in "${checksum_prop_vals[@]}"; do done done -log_must zfs inherit checksum $TESTSRCFS -log_must zfs inherit checksum $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_compress.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_compress.ksh index f155fa2bf..854a64774 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_compress.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_compress.ksh @@ -34,8 +34,21 @@ verify_runnable "both" verify_crossfs_block_cloning +save_tunable BCLONE_STRICT_PROPERTIES + +function cleanup +{ + restore_tunable BCLONE_STRICT_PROPERTIES + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning across datasets with different compression properties" +# Disable strict property checking to allow cross-dataset cloning with different properties +log_must set_tunable32 BCLONE_STRICT_PROPERTIES 0 + for srcprop in "${compress_prop_vals[@]}"; do for dstprop in "${compress_prop_vals[@]}"; do if [[ $srcprop == $dstprop ]]; then @@ -53,7 +66,4 @@ for srcprop in "${compress_prop_vals[@]}"; do done done -log_must zfs inherit compress $TESTSRCFS -log_must zfs inherit compress $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_copies.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_copies.ksh index 5f5ea2960..8c6015aea 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_copies.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_copies.ksh @@ -34,8 +34,23 @@ verify_runnable "both" verify_crossfs_block_cloning +save_tunable BCLONE_STRICT_PROPERTIES + +function cleanup +{ + restore_tunable BCLONE_STRICT_PROPERTIES + log_must zfs inherit copies $TESTSRCFS + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit copies $TESTDSTFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning across datasets with different copies properties" +# Disable strict property checking to allow cross-dataset cloning with different properties +log_must set_tunable32 BCLONE_STRICT_PROPERTIES 0 + log_must zfs set compress=off $TESTSRCFS log_must zfs set compress=off $TESTDSTFS @@ -53,7 +68,4 @@ for srcprop in "${copies_prop_vals[@]}"; do done done -log_must zfs inherit copies $TESTSRCFS -log_must zfs inherit copies $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_recordsize.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_recordsize.ksh index 32211268c..dbc53746f 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_recordsize.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_diffprops_recordsize.ksh @@ -34,8 +34,23 @@ verify_runnable "both" verify_crossfs_block_cloning +save_tunable BCLONE_STRICT_PROPERTIES + +function cleanup +{ + restore_tunable BCLONE_STRICT_PROPERTIES + log_must zfs inherit recordsize $TESTSRCFS + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit recordsize $TESTDSTFS + log_must zfs inherit compress $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning across datasets with different recordsize properties" +# Disable strict property checking to allow cross-dataset cloning with different properties +log_must set_tunable32 BCLONE_STRICT_PROPERTIES 0 + log_must zfs set compress=off $TESTSRCFS log_must zfs set compress=off $TESTDSTFS @@ -59,7 +74,4 @@ for srcprop in "${bclone_recsize_prop_vals[@]}"; do done done -log_must zfs inherit recordsize $TESTSRCFS -log_must zfs inherit recordsize $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_prop_sync.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_prop_sync.ksh index 0cb095060..ffd5912f4 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_prop_sync.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_prop_sync.ksh @@ -34,6 +34,15 @@ verify_runnable "both" verify_crossfs_block_cloning +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit compress $TESTDSTFS + log_must zfs inherit sync $TESTSRCFS + log_must zfs inherit sync $TESTDSTFS +} +log_onexit cleanup + log_assert "Verify block cloning with all sync property settings" log_must zfs set compress=zle $TESTSRCFS @@ -64,7 +73,4 @@ for srcprop in "${sync_prop_vals[@]}"; do done done -log_must zfs inherit sync $TESTSRCFS -log_must zfs inherit sync $TESTDSTFS - log_pass diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases.ksh index 884f08c4f..d18a1bd24 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases.ksh @@ -30,6 +30,13 @@ verify_runnable "both" +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit recordsize $TESTSRCFS +} +log_onexit cleanup + log_assert "Verify various corner cases in block cloning within the same dataset" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases_limited.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases_limited.ksh index 0492a26d2..8dd30586a 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases_limited.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_corner_cases_limited.ksh @@ -30,6 +30,13 @@ verify_runnable "both" +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS + log_must zfs inherit recordsize $TESTSRCFS +} +log_onexit cleanup + log_assert "Verify various corner cases in block cloning within the same dataset" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_data.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_data.ksh index fca990815..45551e046 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_data.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_data.ksh @@ -30,6 +30,12 @@ verify_runnable "both" +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS +} +log_onexit cleanup + log_assert "Verify block cloning properly clones regular files within the same dataset" # Disable compression to make sure we won't use embedded blocks. diff --git a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_embedded.ksh b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_embedded.ksh index a4355e052..8111c62c1 100755 --- a/tests/zfs-tests/tests/functional/bclone/bclone_samefs_embedded.ksh +++ b/tests/zfs-tests/tests/functional/bclone/bclone_samefs_embedded.ksh @@ -30,6 +30,12 @@ verify_runnable "both" +function cleanup +{ + log_must zfs inherit compress $TESTSRCFS +} +log_onexit cleanup + log_assert "Verify block cloning properly clones small files (with embedded blocks) within the same dataset" # Enable ZLE compression to make sure what is the maximum amount of data we