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