diff options
| author | Gary Guo <gary@garyguo.net> | 2026-04-15 21:51:53 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-15 21:51:53 +0000 |
| commit | 1644e2ffd2640fa3e2c191ceaf048a5fc8399493 (patch) | |
| tree | f6104f1bdc6f8e3aa95095701294ae88549ada56 | |
| parent | 4b4ae48f9a286ac536679d68dce9aa46d126249b (diff) | |
| -rw-r--r-- | module/zfs/dbuf.c | 6 | ||||
| -rw-r--r-- | tests/runfiles/common.run | 3 | ||||
| -rwxr-xr-x | tests/test-runner/bin/zts-report.py.in | 1 | ||||
| -rw-r--r-- | tests/zfs-tests/cmd/.gitignore | 1 | ||||
| -rw-r--r-- | tests/zfs-tests/cmd/Makefile.am | 2 | ||||
| -rw-r--r-- | tests/zfs-tests/cmd/clone_after_trunc.c | 117 | ||||
| -rw-r--r-- | tests/zfs-tests/include/commands.cfg | 1 | ||||
| -rw-r--r-- | tests/zfs-tests/tests/Makefile.am | 1 | ||||
| -rwxr-xr-x | tests/zfs-tests/tests/functional/block_cloning/block_cloning_after_trunc.ksh | 31 |
9 files changed, 161 insertions, 2 deletions
diff --git a/module/zfs/dbuf.c b/module/zfs/dbuf.c index 980c35822328..a4fe3e519700 100644 --- a/module/zfs/dbuf.c +++ b/module/zfs/dbuf.c @@ -1480,8 +1480,12 @@ dbuf_read_hole(dmu_buf_impl_t *db, dnode_t *dn, blkptr_t *bp) * Recheck BP_IS_HOLE() after dnode_block_freed() in case dnode_sync() * processes the delete record and clears the bp while we are waiting * for the dn_mtx (resulting in a "no" from block_freed). + * + * If bp != db->db_blkptr, it means that it was overridden (by a block + * clone or direct I/O write). We cannot rely on dnode_block_freed as + * the range can be freed in an earlier TXG but overridden in later. */ - if (!is_hole && db->db_level == 0) + if (!is_hole && db->db_level == 0 && bp == db->db_blkptr) is_hole = dnode_block_freed(dn, db->db_blkid) || BP_IS_HOLE(bp); if (is_hole) { diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 37c305f1a66a..243d28e8bc49 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -84,7 +84,8 @@ tests = ['block_cloning_clone_mmap_cached', 'block_cloning_replay', 'block_cloning_replay_encrypted', 'block_cloning_lwb_buffer_overflow', 'block_cloning_clone_mmap_write', 'block_cloning_rlimit_fsize', 'block_cloning_large_offset', - 'block_cloning_after_device_removal'] + 'block_cloning_after_device_removal', + 'block_cloning_after_trunc'] tags = ['functional', 'block_cloning'] [tests/functional/bootfs] diff --git a/tests/test-runner/bin/zts-report.py.in b/tests/test-runner/bin/zts-report.py.in index 3de394bbc454..874a23a87574 100755 --- a/tests/test-runner/bin/zts-report.py.in +++ b/tests/test-runner/bin/zts-report.py.in @@ -320,6 +320,7 @@ elif sys.platform.startswith('linux'): 'bclone/bclone_samefs_data': ['SKIP', cfr_reason], 'bclone/bclone_samefs_embedded': ['SKIP', cfr_reason], 'bclone/bclone_samefs_hole': ['SKIP', cfr_reason], + 'block_cloning/block_cloning_after_trunc': ['SKIP', cfr_reason], 'block_cloning/block_cloning_clone_mmap_cached': ['SKIP', cfr_reason], 'block_cloning/block_cloning_clone_mmap_write': ['SKIP', cfr_reason], diff --git a/tests/zfs-tests/cmd/.gitignore b/tests/zfs-tests/cmd/.gitignore index 335e4ceba282..4bdca0acf52b 100644 --- a/tests/zfs-tests/cmd/.gitignore +++ b/tests/zfs-tests/cmd/.gitignore @@ -2,6 +2,7 @@ /btree_test /chg_usr_exec /clonefile +/clone_after_trunc /clone_mmap_cached /clone_mmap_write /crypto_test diff --git a/tests/zfs-tests/cmd/Makefile.am b/tests/zfs-tests/cmd/Makefile.am index 9683834f8e92..c4155ca3cacd 100644 --- a/tests/zfs-tests/cmd/Makefile.am +++ b/tests/zfs-tests/cmd/Makefile.am @@ -34,6 +34,8 @@ scripts_zfs_tests_bin_PROGRAMS += %D%/crypto_test %C%_crypto_test_SOURCES = %D%/crypto_test.c %C%_crypto_test_LDADD = libzpool.la +scripts_zfs_tests_bin_PROGRAMS += %D%/clone_after_trunc +%C%_clone_after_trunc_LDADD = -lpthread if WANT_DEVNAME2DEVID scripts_zfs_tests_bin_PROGRAMS += %D%/devname2devid diff --git a/tests/zfs-tests/cmd/clone_after_trunc.c b/tests/zfs-tests/cmd/clone_after_trunc.c new file mode 100644 index 000000000000..631432928984 --- /dev/null +++ b/tests/zfs-tests/cmd/clone_after_trunc.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: CDDL-1.0 + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <pthread.h> +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/stat.h> + +#if defined(_GNU_SOURCE) && defined(__linux__) +_Static_assert(sizeof (loff_t) == sizeof (off_t), + "loff_t and off_t must be the same size"); +#endif + +ssize_t +copy_file_range(int, off_t *, int, off_t *, size_t, unsigned int) + __attribute__((weak)); + +#define FILE_SIZE (1024 * 1024) +#define RECORD_SIZE (128 * 1024) +#define NUM_THREADS 64 + +const char *dir; +volatile int failed; + +static void * +run_test(void *arg) +{ + int thread_id = (int)(long)arg; + + char src_path[PATH_MAX], dst_path[PATH_MAX]; + snprintf(src_path, PATH_MAX, "%s/src-%d", dir, thread_id); + snprintf(dst_path, PATH_MAX, "%s/dst-%d", dir, thread_id); + + unsigned char *write_buf = malloc(FILE_SIZE); + unsigned char *read_buf = malloc(FILE_SIZE); + + // Write out expected data. + memset(write_buf, 0xAA, FILE_SIZE); + int src = open(src_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (write(src, write_buf, FILE_SIZE) != FILE_SIZE) + perror("write"); + close(src); + + // Create destination file so we exercise O_TRUNC. + int dst = open(dst_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (write(dst, write_buf, FILE_SIZE) != FILE_SIZE) + perror("write"); + fsync(dst); + close(dst); + + // Open file with O_TRUNC and perform copy. + src = open(src_path, O_RDONLY); + dst = open(dst_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + off_t off_in = 0, off_out = 0; + ssize_t ret = + copy_file_range(src, &off_in, dst, &off_out, FILE_SIZE, 0); + if (ret != FILE_SIZE) + perror("copy_file_range"); + close(src); + close(dst); + + // Read back + dst = open(dst_path, O_RDONLY); + if (read(dst, read_buf, FILE_SIZE) != FILE_SIZE) + perror("read"); + close(dst); + + // Bug check + if (memcmp(write_buf, read_buf, FILE_SIZE) != 0) { + failed = 1; + fprintf(stderr, "[%d]: FAIL\n", thread_id); + + int all_zeros = 1; + for (int i = 0; i < RECORD_SIZE; i++) { + if (read_buf[i] != 0) { + all_zeros = 0; + break; + } + } + + if (all_zeros) { + fprintf(stderr, "[%d]: ALL ZERO\n", thread_id); + } + } + + unlink(src_path); + unlink(dst_path); + free(write_buf); + free(read_buf); + return (NULL); +} + +int +main(int argc, const char **argv) +{ + if (argc < 2) { + fprintf(stderr, "usage: %s <dir>\n", argv[0]); + return (1); + } + dir = argv[1]; + + pthread_t threads[NUM_THREADS]; + + for (int i = 0; i < NUM_THREADS; i++) { + pthread_create(&threads[i], NULL, run_test, (void *)(long)i); + } + for (int i = 0; i < NUM_THREADS; i++) { + pthread_join(threads[i], NULL); + } + + return (failed); +} diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index ed3e9250ae5b..a52cacec224a 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -187,6 +187,7 @@ export ZFSTEST_FILES_COMMON='badsend btree_test chg_usr_exec clonefile + clone_after_trunc clone_mmap_cached clone_mmap_write crypto_test diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 99c42e0defb5..cf04950a9612 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -495,6 +495,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/block_cloning/block_cloning_rlimit_fsize.ksh \ functional/block_cloning/block_cloning_large_offset.ksh \ functional/block_cloning/block_cloning_after_device_removal.ksh \ + functional/block_cloning/block_cloning_after_trunc.ksh \ functional/bootfs/bootfs_001_pos.ksh \ functional/bootfs/bootfs_002_neg.ksh \ functional/bootfs/bootfs_003_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/block_cloning/block_cloning_after_trunc.ksh b/tests/zfs-tests/tests/functional/block_cloning/block_cloning_after_trunc.ksh new file mode 100755 index 000000000000..977ec16042b7 --- /dev/null +++ b/tests/zfs-tests/tests/functional/block_cloning/block_cloning_after_trunc.ksh @@ -0,0 +1,31 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/block_cloning/block_cloning.kshlib + +# +# DESCRIPTION: +# When a block is truncated and then cloned to, a read data corruption can occur. +# This is a regression test for #18412. +# + +verify_runnable "global" + +claim="No read data corruption when cloning blocks after a truncate" + +function cleanup +{ + datasetexists $TESTPOOL && destroy_pool $TESTPOOL +} + +log_onexit cleanup + +log_must zpool create -o feature@block_cloning=enabled $TESTPOOL $DISKS + +# Run for a few times to increase the likelihood of bug triggering. +for i in {0..50}; do + log_must clone_after_trunc /$TESTPOOL/ +done + +log_pass $claim |
