Subject: bug#79139: closed (Re: bug#79139: cp --reflink truncates sparse files on ZFS)

From: [email-masked] (GNU bug Tracking System)
Date: Sat, 02 Aug 2025 00:18:02 +0000

Your bug report

#79139: cp --reflink truncates sparse files on ZFS

which was filed against the coreutils package, has been closed.

The explanation is attached below, along with your original report.
If you require more details, please reply to [email-masked].

-- 
79139: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=79139
GNU Bug Tracking System
Contact [email-masked] with problems

Subject: Re: bug#79139: cp --reflink truncates sparse files on ZFS
From: Paul Eggert <[email-masked]>
To: Collin Funk <[email-masked]>
Cc: [email-masked],Pádraig Brady <[email-masked]>, Leah Neukirchen <[email-masked]>
Date: Fri, 1 Aug 2025 17:17:16 -0700
List-Id: <debbugs-submit.debbugs.gnu.org>

On 2025-08-01 15:05, Collin Funk wrote:
> I was hoping that file could be made a tiny stub, due to the
> workarounds for Linux 4.19 being mostly unnecessary now that it is EOL.
> But now we have a new problem to deal with. :)

That we do. But we can more thorougly stubify the old Linux kernel bug 
workaround while we're in the neighborhood. Probably best not to remove 
it entirely as RHEL 8 still uses the no-longer-supported kernel.

To do that, I installed the attached patches into Gnulib and propagated 
them into coreutils.

Boldly closing the bug report. Thanks, Leah, for reporting it. That one 
was quite a whopper.

From 626f229915b114731cc4c9d9bda9eaa82d58180b Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Fri, 1 Aug 2025 14:46:51 -0700
Subject: [PATCH 1/2] copy-file-range: tune for more-modern kernels
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* lib/copy-file-range.c [__linux__ && HAVE_COPY_FILE_RANGE]:
Include <linux/version.h>.  Include <sys/utsname.h>
only for kernel 5.2 and earlier.
(CHECK_LINUX_KERNEL_VERSION): New macro.
(copy_file_range) [__linux__ && HAVE_COPY_FILE_RANGE]:
Call utsname only when built for kernel 5.2 and earlier.
(copy_file_range) [!__linux__ && HAVE_COPY_FILE_RANGE]:
Use underlying copy_file_range.  This doesn’t change
behavior since the code is not compiled in this case,
but it makes the code a bit cleaner.
* modules/copy-file-range (Depends-on): Add bool.
---
 ChangeLog               | 15 +++++++++++++
 lib/copy-file-range.c   | 48 +++++++++++++++++++++++++----------------
 m4/copy-file-range.m4   |  3 +--
 modules/copy-file-range |  1 +
 4 files changed, 47 insertions(+), 20 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 60b8d08d37..b33d7fd00b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,18 @@
+2025-08-01  Paul Eggert  <[email-masked]>
+
+	copy-file-range: tune for more-modern kernels
+	* lib/copy-file-range.c [__linux__ && HAVE_COPY_FILE_RANGE]:
+	Include <linux/version.h>.  Include <sys/utsname.h>
+	only for kernel 5.2 and earlier.
+	(CHECK_LINUX_KERNEL_VERSION): New macro.
+	(copy_file_range) [__linux__ && HAVE_COPY_FILE_RANGE]:
+	Call utsname only when built for kernel 5.2 and earlier.
+	(copy_file_range) [!__linux__ && HAVE_COPY_FILE_RANGE]:
+	Use underlying copy_file_range.  This doesn’t change
+	behavior since the code is not compiled in this case,
+	but it makes the code a bit cleaner.
+	* modules/copy-file-range (Depends-on): Add bool.
+
 2025-08-01  Bruno Haible  <[email-masked]>
 
 	sigsegv: Use new ioctl available in Linux >= 6.11.
diff --git a/lib/copy-file-range.c b/lib/copy-file-range.c
index 8e0c644c08..73e02db1c1 100644
--- a/lib/copy-file-range.c
+++ b/lib/copy-file-range.c
@@ -21,7 +21,13 @@
 #include <errno.h>
 
 #if defined __linux__ && HAVE_COPY_FILE_RANGE
+# include <linux/version.h>
 # include <sys/utsname.h>
+# if LINUX_VERSION_CODE < KERNEL_VERSION (5, 3, 0)
+#  define CHECK_LINUX_KERNEL_VERSION true
+# else
+#  define CHECK_LINUX_KERNEL_VERSION false
+# endif
 #endif
 
 ssize_t
@@ -31,32 +37,38 @@ copy_file_range (int infd, off_t *pinoff,
 {
 #undef copy_file_range
 
-#if defined __linux__ && HAVE_COPY_FILE_RANGE
+#if HAVE_COPY_FILE_RANGE
+  bool ok = true;
+
+# if CHECK_LINUX_KERNEL_VERSION
   /* The implementation of copy_file_range (which first appeared in
      Linux kernel release 4.5) had many issues before release 5.3
      <https://lwn.net/Articles/789527/>, so fail with ENOSYS for Linux
      kernels 5.2 and earlier.
 
-     This workaround, and the configure-time check for Linux, can be
-     removed when such kernels (released March 2016 through September
-     2019) are no longer a consideration.  As of January 2021, the
-     furthest-future planned kernel EOL is December 2024 for kernel
-     release 4.19.  */
+     This workaround can be removed when such kernels (released March
+     2016 through September 2019) are no longer a consideration.
+     Although all such kernels have reached EOL, some distros use
+     older kernels.  For example, RHEL 8 uses kernel 4.18 and has an
+     EOL of 2029.  */
 
-    static signed char ok;
+  static signed char kernel_ok;
+  if (! kernel_ok)
+    {
+      struct utsname name;
+      uname (&name);
+      char *p = name.release;
+      kernel_ok = ((p[1] != '.' || '5' < p[0]
+                    || (p[0] == '5' && (p[3] != '.' || '2' < p[2])))
+                   ? 1 : -1);
+    }
 
-    if (! ok)
-      {
-        struct utsname name;
-        uname (&name);
-        char *p = name.release;
-        ok = ((p[1] != '.' || '5' < p[0]
-               || (p[0] == '5' && (p[3] != '.' || '2' < p[2])))
-              ? 1 : -1);
-      }
+  if (kernel_ok < 0)
+    ok = false;
+# endif
 
-    if (0 < ok)
-      return copy_file_range (infd, pinoff, outfd, poutoff, length, flags);
+  if (ok)
+    return copy_file_range (infd, pinoff, outfd, poutoff, length, flags);
 #endif
 
   /* There is little need to emulate copy_file_range with read+write,
diff --git a/m4/copy-file-range.m4 b/m4/copy-file-range.m4
index 13081d9b11..ec17fc68c7 100644
--- a/m4/copy-file-range.m4
+++ b/m4/copy-file-range.m4
@@ -52,8 +52,7 @@ AC_DEFUN([gl_FUNC_COPY_FILE_RANGE],
 
     case $host_os in
       linux*)
-        # See copy-file-range.c comment re pre-5.3 Linux kernel bugs.
-        # We should be able to remove this hack in 2025.
+        # See copy-file-range.c comment re Linux kernel bugs.
         REPLACE_COPY_FILE_RANGE=1;;
     esac
   fi
diff --git a/modules/copy-file-range b/modules/copy-file-range
index 12b6fcb86e..573558a945 100644
--- a/modules/copy-file-range
+++ b/modules/copy-file-range
@@ -6,6 +6,7 @@ lib/copy-file-range.c
 m4/copy-file-range.m4
 
 Depends-on:
+bool
 largefile
 unistd-h
 
-- 
2.48.1

From 948ba80168293e3d8a905eeffb0ab61424c4c374 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Fri, 1 Aug 2025 16:01:13 -0700
Subject: [PATCH 2/2] copy-file-range: work around glibc bug 33245

Problem reported by Leah Neukirchen <https://bugs.gnu.org/79139>.
* lib/copy-file-range.c: Include sys-limits.h.
(copy_file_range) [glibc 2.42 and earlier]:
Copy at most SYS_BUFSIZE_MAX bytes.
* m4/copy-file-range.m4 (gl_FUNC_COPY_FILE_RANGE):
Replace on glibc platforms, as well as on Linux platforms.
* modules/copy-file-range (Files): Add lib/sys-limits.h.
---
 ChangeLog               |  9 +++++++++
 lib/copy-file-range.c   | 17 ++++++++++++++++-
 m4/copy-file-range.m4   |  4 ++--
 modules/copy-file-range |  1 +
 4 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index b33d7fd00b..e60a387611 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,14 @@
 2025-08-01  Paul Eggert  <[email-masked]>
 
+	copy-file-range: work around glibc bug 33245
+	Problem reported by Leah Neukirchen <https://bugs.gnu.org/79139>.
+	* lib/copy-file-range.c: Include sys-limits.h.
+	(copy_file_range) [glibc 2.42 and earlier]:
+	Copy at most SYS_BUFSIZE_MAX bytes.
+	* m4/copy-file-range.m4 (gl_FUNC_COPY_FILE_RANGE):
+	Replace on glibc platforms, as well as on Linux platforms.
+	* modules/copy-file-range (Files): Add lib/sys-limits.h.
+
 	copy-file-range: tune for more-modern kernels
 	* lib/copy-file-range.c [__linux__ && HAVE_COPY_FILE_RANGE]:
 	Include <linux/version.h>.  Include <sys/utsname.h>
diff --git a/lib/copy-file-range.c b/lib/copy-file-range.c
index 73e02db1c1..5c12f6f715 100644
--- a/lib/copy-file-range.c
+++ b/lib/copy-file-range.c
@@ -30,6 +30,8 @@
 # endif
 #endif
 
+#include "sys-limits.h"
+
 ssize_t
 copy_file_range (int infd, off_t *pinoff,
                  int outfd, off_t *poutoff,
@@ -68,7 +70,20 @@ copy_file_range (int infd, off_t *pinoff,
 # endif
 
   if (ok)
-    return copy_file_range (infd, pinoff, outfd, poutoff, length, flags);
+    {
+#   if defined __GLIBC__ && ! (2 < __GLIBC__ + (43 <= __GLIBC_MINOR__))
+      /* Work around glibc bug 33245
+         <https://sourceware.org/bugzilla/show_bug.cgi?id=33245>.
+         This bug is present in glibc 2.42 (2025) and fixed in 2.43,
+         so this workaround, and the configure-time check for glibc,
+         can be removed once glibc 2.42 and earlier is no longer a
+         consideration.  Perhaps in 2040.  */
+      if (SYS_BUFSIZE_MAX < length)
+        length = SYS_BUFSIZE_MAX;
+# endif
+
+      return copy_file_range (infd, pinoff, outfd, poutoff, length, flags);
+    }
 #endif
 
   /* There is little need to emulate copy_file_range with read+write,
diff --git a/m4/copy-file-range.m4 b/m4/copy-file-range.m4
index ec17fc68c7..85cfef71f4 100644
--- a/m4/copy-file-range.m4
+++ b/m4/copy-file-range.m4
@@ -51,8 +51,8 @@ AC_DEFUN([gl_FUNC_COPY_FILE_RANGE],
       [Define to 1 if the function copy_file_range exists.])
 
     case $host_os in
-      linux*)
-        # See copy-file-range.c comment re Linux kernel bugs.
+      *-gnu* | gnu* | linux*)
+        # See copy-file-range.c comment re glibc and Linux kernel bugs.
         REPLACE_COPY_FILE_RANGE=1;;
     esac
   fi
diff --git a/modules/copy-file-range b/modules/copy-file-range
index 573558a945..e3512eaffa 100644
--- a/modules/copy-file-range
+++ b/modules/copy-file-range
@@ -3,6 +3,7 @@ Copy parts of files
 
 Files:
 lib/copy-file-range.c
+lib/sys-limits.h
 m4/copy-file-range.m4
 
 Depends-on:
-- 
2.48.1

Subject: cp --reflink truncates sparse files on ZFS
From: Leah Neukirchen <[email-masked]>
To: [email-masked]
Date: Fri, 01 Aug 2025 17:00:36 +0200
List-Id: <debbugs-submit.debbugs.gnu.org>

Hello,

I found the following issue with coreutils 9.7, Linux 6.12.40-1-lts,
zfs 2.3.3 on Arch x86_64, glibc 2.42:

Copying a file with sparse holes using "cp --reflink=auto" truncates
the file before the final segment.  The relevant strace is:

openat(AT_FDCWD, "celestis.img", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/.zfs/snapshot/pre-fixup/var/lib/libvirt/images/celestis.img", {st_mode=S_IFREG|0644,>
openat(AT_FDCWD, "/.zfs/snapshot/pre-fixup/var/lib/libvirt/images/celestis.img", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=137438953472, ...}) = 0
openat(AT_FDCWD, "celestis.img", O_WRONLY|O_CREAT|O_EXCL, 0644) = 4
ioctl(4, BTRFS_IOC_CLONE or FICLONE, 3) = -1 EXDEV (Invalid cross-device link)
fstat(4, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
lseek(3, 0, SEEK_DATA)                  = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
lseek(3, 0, SEEK_HOLE)                  = 131072
lseek(3, 0, SEEK_SET)                   = 0
copy_file_range(3, NULL, 4, NULL, 131072, 0) = 131072
lseek(3, 131072, SEEK_DATA)             = 1048576
lseek(3, 1048576, SEEK_HOLE)            = 1179648
lseek(3, 1048576, SEEK_SET)             = 1048576
lseek(4, 917504, SEEK_CUR)              = 1048576
copy_file_range(3, NULL, 4, NULL, 131072, 0) = 131072
lseek(3, 1179648, SEEK_DATA)            = 4194304
lseek(3, 4194304, SEEK_HOLE)            = 16646144
lseek(3, 4194304, SEEK_SET)             = 4194304
lseek(4, 3014656, SEEK_CUR)             = 4194304
copy_file_range(3, NULL, 4, NULL, 12451840, 0) = 12451840
lseek(3, 16646144, SEEK_DATA)           = 134217728
lseek(3, 134217728, SEEK_HOLE)          = 137438953472
lseek(3, 134217728, SEEK_SET)           = 134217728
lseek(4, 117571584, SEEK_CUR)           = 134217728
copy_file_range(3, NULL, 4, NULL, 137304735744, 0) = 137304735744
mmap(NULL, 270336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76c5df0ce000
read(3, "", 262144)                     = 0
ftruncate(4, 134217728)                 = 0
close(4)                                = 0
close(3)                                = 0

As we can see, there's a hole from 16646144 to 134217728, then data up
to the end at 137438953472 (= the total file size).  Both fd are thus
moved to 134217728, and a copy_file_range for the rest of the file is
issued and successful.

However, in the end the file is truncated to the first 128MB... why?

For comparison, a plain cat simply does this:

fstat(1, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
openat(AT_FDCWD, "/.zfs/snapshot/pre-fixup/var/lib/libvirt/images/celestis.img", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=137438953472, ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
copy_file_range(3, NULL, 1, NULL, 9223372035781033984, 0) = 137438953472
mmap(NULL, 270336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7e7af83a9000
read(3, "", 262144)                     = 0
munmap(0x7e7af83a9000, 270336)          = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0

This works correctly and the source and destination agree in the end.
Likewise for xcp(1), which uses copy_file_range in 1MB blocks by
default and does not care for holes.

Thus I think this is a logic bug in cp and not a ZFS issue.

Do not hesitate to contact me if you inquire further details.

Thanks,
-- 
Leah Neukirchen  <[email-masked]>  https://leahneukirchen.org/