On 2026-09-02 10:12, Pádraig Brady wrote:
we should remove ENOSPC from being considered as a terminal error

Yes, but it's not just ENOSPC; it's also EDQUOT etc. That is, as far as I can 
see, no error from an allocation group necessarily means there must be an error 
from an ordinary read+write copy.

Also, it strikes me that if this sort of thing happens, people might want to 
know details when debugging.

So I installed the attached. The first treats EDQUOT etc. like ENOSPC; the 
second reports the errno when --debug is used.
From be8600bb248b71c927582d61849d027eea423445 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Wed, 2 Sep 2026 12:06:18 -0700
Subject: [PATCH 1/2] cp: treat EDQUOT etc. like ENOSPC

Assume that XFS allocation groups can also fail with EDQUOT etc.,
so treat those failures like ENOSPC.
* src/copy.c (is_terminal_error): Remove; no longer used.
(handle_clone_fail): Remove DEST_DESC arg; no longer needed.
All uses changed.  Simplify by acting as if is_terminal_error
always returns false.  Change "!= 0" to "< 0" when either will do
for a syscall, as a matter of convention.
---
 NEWS       |  8 ++++----
 src/copy.c | 56 +++++++++++++++++-------------------------------------
 2 files changed, 21 insertions(+), 43 deletions(-)

diff --git a/NEWS b/NEWS
index cc9bcf248..0e4ad1d94 100644
--- a/NEWS
+++ b/NEWS
@@ -12,10 +12,10 @@ GNU coreutils NEWS                                    -*- outline -*-
   mistakenly exit with a nonzero status.
   [This bug was present in "the beginning".]
 
-  'cp`, 'install', and 'mv' will fall back to a standard copy in the case where
-  a clone fails with ENOSPC.  I.e., with --reflink=auto (enabled by default)
-  on COW supporting file systems.  E.g. with XFS, a clone can exhaust
-  metadata space in an allocation group, while a standard copy works.
+  'cp`, 'install', and 'mv' now fall back to a standard copy
+  if a --reflink=auto clone fails for any reason, including ENOSPC.
+  E.g., with XFS, a clone can exhaust metadata space in an allocation
+  group, while a standard copy works.
   [bug introduced in coreutils-9.2]
 
   'cut -d' with multiple multi-byte delimiter options specified
diff --git a/src/copy.c b/src/copy.c
index 097d6af97..7503f4017 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -212,22 +212,6 @@ follow_fstatat (int dirfd, char const *filename, struct stat *st, int flags)
   return result;
 }
 
-/* Whether an errno value ERR, set by FICLONE or copy_file_range,
-   indicates that the copying operation has terminally failed, even
-   though it was invoked correctly (so that, e.g, EBADF cannot occur)
-   and even though !is_CLONENOTSUP (ERR).
-
-   Note ENOSPC is _not_ included as that can merely mean that
-   metadata space is exhausted, while a standard copy may proceed.
-   E.g. XFS has Allocation Groups where a clone may fail, but a
-   copy (to other groups) may succeed.  */
-
-static bool
-is_terminal_error (int err)
-{
-  return err == EIO || err == ENOMEM || err == EDQUOT;
-}
-
 /* Perform the O(1) btrfs clone operation, if possible.
    Upon success, return 0.  Otherwise, return -1 and set errno.  */
 static inline int
@@ -684,34 +668,28 @@ fd_has_acl (int fd)
 static bool
 handle_clone_fail (int dst_dirfd, char const *dst_relname,
                    char const *src_name, char const *dst_name,
-                   int dest_desc, bool new_dst, enum Reflink_type reflink_mode)
+                   bool new_dst, enum Reflink_type reflink_mode)
 {
-  /* When the clone operation fails, report failure only with errno values
-     known to mean trouble when the clone is supported and called properly.
-     Do not report failure merely because !is_CLONENOTSUP (errno),
-     as systems may yield oddball errno values here with FICLONE,
-     and is_CLONENOTSUP is not appropriate for fclonefileat.  */
-  bool report_failure = is_terminal_error (errno);
+  /* Record failure for debugging, but return false only if --reflink=always.
+     No errno value is serious enough to give up on read+write copying,
+     which can succeed even if cloning fails due to ENOSPC etc.
+     E.g., XFS has Allocation Groups where a clone may fail but a copy
+     (to other groups) may succeed.  */
 
-  if (reflink_mode == REFLINK_ALWAYS || report_failure)
-    error (0, errno, _("failed to clone %s from %s"),
-           quoteaf_n (0, dst_name), quoteaf_n (1, src_name));
+  copy_debug.reflink = COPY_DEBUG_UNSUPPORTED;
+
+  if (reflink_mode != REFLINK_ALWAYS)
+    return true;
+
+  error (0, errno, _("failed to clone %s from %s"),
+         quoteaf_n (0, dst_name), quoteaf_n (1, src_name));
 
   /* Remove the destination if cp --reflink=always created it
      but cloned no data.  */
   if (new_dst /* currently not for fclonefileat().  */
-      && reflink_mode == REFLINK_ALWAYS
-      && ((! report_failure) || lseek (dest_desc, 0, SEEK_END) == 0)
-      && unlinkat (dst_dirfd, dst_relname, 0) != 0 && errno != ENOENT)
+      && unlinkat (dst_dirfd, dst_relname, 0) < 0 && errno != ENOENT)
     error (0, errno, _("cannot remove %s"), quoteaf (dst_name));
-
-  if (! report_failure)
-    copy_debug.reflink = COPY_DEBUG_UNSUPPORTED;
-
-  if (reflink_mode == REFLINK_ALWAYS || report_failure)
-    return false;
-
-  return true;
+  return false;
 }
 
 /* Copy a regular file from SRC_NAME to DST_NAME aka DST_DIRFD+DST_RELNAME.
@@ -928,7 +906,7 @@ copy_reg (char const *src_name, char const *dst_name,
                 }
               if (! handle_clone_fail (dst_dirfd, dst_relname, src_name,
                                        dst_name,
-                                       -1, false /* We didn't create dst  */,
+                                       false /* We didn't create dst  */,
                                        x->reflink_mode))
                 {
                   return_val = false;
@@ -1018,7 +996,7 @@ copy_reg (char const *src_name, char const *dst_name,
       else
         {
           if (! handle_clone_fail (dst_dirfd, dst_relname, src_name, dst_name,
-                                   dest_desc, *new_dst, x->reflink_mode))
+                                   *new_dst, x->reflink_mode))
            {
              return_val = false;
              goto close_src_and_dst_desc;
-- 
2.55.0

From cefa8e8fe8ff7634698f71f77bdb170d949cdfda Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Wed, 2 Sep 2026 12:22:20 -0700
Subject: [PATCH 2/2] cp: --debug now reports clone/offload fail errno

* src/copy.h: Use errno values for syscall failures, and
negative values otherwise.  That way, debugging output can
contain more info when clone or offload ops fail.
All uses changed.
---
 src/copy-file-data.c |  4 ++--
 src/copy.c           | 11 +++++++----
 src/copy.h           | 19 ++++++++++++-------
 3 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/copy-file-data.c b/src/copy-file-data.c
index 9d2583344..e7e3108af 100644
--- a/src/copy-file-data.c
+++ b/src/copy-file-data.c
@@ -147,6 +147,8 @@ sparse_copy (int src_fd, int dest_fd, char **abuf, idx_t buf_size,
           }
         if (n_copied < 0)
           {
+            debug->offload = errno;
+
             /* Don’t treat EFBIG as a reportable error from copy_file_range.
                If the input is at EOF and the output position is 2**63 - 1,
                copy_file_range (ifd, NULL, ofd, NULL, 2146435072, 0)
@@ -155,8 +157,6 @@ sparse_copy (int src_fd, int dest_fd, char **abuf, idx_t buf_size,
             if (errno == EFBIG)
               break;
 
-            debug->offload = COPY_DEBUG_UNSUPPORTED;
-
             /* Consider operation unsupported only if no data copied.
                For example, EPERM could occur if copy_file_range not enabled
                in seccomp filters, so retry with a standard copy.  EPERM can
diff --git a/src/copy.c b/src/copy.c
index 7503f4017..ce6aa8e1a 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -139,12 +139,15 @@ copy_debug_string (enum copy_debug_val debug_val)
     case COPY_DEBUG_NO: return "no";
     case COPY_DEBUG_YES: return "yes";
     case COPY_DEBUG_AVOIDED: return "avoided";
-    case COPY_DEBUG_UNSUPPORTED: return "unsupported";
     case COPY_DEBUG_UNKNOWN: return "unknown";
 
     case COPY_DEBUG_EXTERNAL:
     case COPY_DEBUG_EXTERNAL_INTERNAL:
-    default: affirm (false);
+    case COPY_DEBUG_MAX:
+    default:
+      if (0 < debug_val)
+        return strerror (debug_val);
+      affirm (false);
     }
 }
 
@@ -160,7 +163,7 @@ copy_debug_sparse_string (enum copy_debug_val debug_val)
     case COPY_DEBUG_UNKNOWN: return "unknown";
 
     case COPY_DEBUG_AVOIDED:
-    case COPY_DEBUG_UNSUPPORTED:
+    case COPY_DEBUG_MAX:
     default: affirm (false);
     }
 }
@@ -676,7 +679,7 @@ handle_clone_fail (int dst_dirfd, char const *dst_relname,
      E.g., XFS has Allocation Groups where a clone may fail but a copy
      (to other groups) may succeed.  */
 
-  copy_debug.reflink = COPY_DEBUG_UNSUPPORTED;
+  copy_debug.reflink = errno;
 
   if (reflink_mode != REFLINK_ALWAYS)
     return true;
diff --git a/src/copy.h b/src/copy.h
index f2d10fefd..ead096683 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -308,13 +308,18 @@ struct cp_options
 
 enum copy_debug_val
   {
-   COPY_DEBUG_UNKNOWN,
-   COPY_DEBUG_NO,
-   COPY_DEBUG_YES,
-   COPY_DEBUG_EXTERNAL,
-   COPY_DEBUG_EXTERNAL_INTERNAL,
-   COPY_DEBUG_AVOIDED,
-   COPY_DEBUG_UNSUPPORTED,
+   /* COPY_DEBUG_MAX is a dummy that ensures that any errno
+      value can be assigned to an enum copy_debug_val.
+      The other enum constants are negative,
+      so that they do not collide with errno values.  */
+   COPY_DEBUG_MAX = INT_MAX,
+
+   COPY_DEBUG_UNKNOWN = -1,
+   COPY_DEBUG_NO = -2,
+   COPY_DEBUG_YES = -3,
+   COPY_DEBUG_EXTERNAL = -4,
+   COPY_DEBUG_EXTERNAL_INTERNAL = -5,
+   COPY_DEBUG_AVOIDED = -6,
   };
 
 /* debug info about the last file copy.  */
-- 
2.55.0

Reply via email to