[PATCH 3/3] NFS: stamp after the server's ctime following a CLONE

From: Jeff Layton

Date: Fri Nov 06 2026 - 15:52:28 EST


nfs42_copy_dest_done() stamps the client's own c/mtime when it holds
delegated timestamps, and the CLONE reply's attributes are then
filtered out. A server that stamps c/mtime on CLONE (as nfsd did before
"nfsd: mark the delegation written on COPY/CLONE instead of stamping
c/mtime", and as btrfs did before "btrfs: honor FMODE_NOCMTIME when
cloning") can end up with a ctime newer than the client's coarse stamp,
which it won't move back when the holder returns its times.

Pass the CLONE reply's attributes to nfs42_copy_dest_done() and raise
the cached ctime to the server's before stamping. COPY returns no
attributes, so it passes NULL.

Fixes: 6e7daa3dad29 ("NFSv4.2: fix CLONE/COPY attrs in presence of delegated attributes")
Signed-off-by: Jeff Layton <jlayton@xxxxxxxxxx>
---
fs/nfs/delegation.h | 2 ++
fs/nfs/inode.c | 5 +++--
fs/nfs/nfs42proc.c | 12 +++++++++---
3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/fs/nfs/delegation.h b/fs/nfs/delegation.h
index fba4699952b8..cd6142939d34 100644
--- a/fs/nfs/delegation.h
+++ b/fs/nfs/delegation.h
@@ -95,6 +95,8 @@ void nfs4_inode_make_writeable(struct inode *inode);
void nfs_update_delegated_atime(struct inode *inode);
void nfs_update_delegated_mtime(struct inode *inode);
void nfs_update_delegated_mtime_locked(struct inode *inode);
+void nfs_delegated_ctime_raise(struct inode *inode,
+ const struct nfs_fattr *fattr);

static inline int nfs_have_read_or_write_delegation(struct inode *inode)
{
diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
index 8b86d93f8101..0175e7719ab4 100644
--- a/fs/nfs/inode.c
+++ b/fs/nfs/inode.c
@@ -653,8 +653,8 @@ nfs_delegated_ctime_after(struct inode *inode, const struct timespec64 *old)
* cached ctime to the server's so that the next delegated stamp lands
* after it.
*/
-static void nfs_delegated_ctime_raise(struct inode *inode,
- const struct nfs_fattr *fattr)
+void nfs_delegated_ctime_raise(struct inode *inode,
+ const struct nfs_fattr *fattr)
{
struct timespec64 ctime = inode_get_ctime(inode);

@@ -662,6 +662,7 @@ static void nfs_delegated_ctime_raise(struct inode *inode,
timespec64_compare(&fattr->ctime, &ctime) > 0)
inode_set_ctime_to_ts(inode, fattr->ctime);
}
+EXPORT_SYMBOL_GPL(nfs_delegated_ctime_raise);

static void nfs_set_timestamps_to_ts(struct inode *inode, struct iattr *attr)
{
diff --git a/fs/nfs/nfs42proc.c b/fs/nfs/nfs42proc.c
index ab86246fc364..3ab36730688e 100644
--- a/fs/nfs/nfs42proc.c
+++ b/fs/nfs/nfs42proc.c
@@ -380,6 +380,7 @@ static int process_copy_commit(struct file *dst, loff_t pos_dst,
* @pos: destination offset
* @len: copy length
* @oldsize: length of the file prior to clone/copy
+ * @fattr: destination attributes from the reply, or NULL
*
* Punch a hole in the inode page cache, so that the NFS client will
* know to retrieve new data.
@@ -387,7 +388,7 @@ static int process_copy_commit(struct file *dst, loff_t pos_dst,
* invalid cached values for change attribute, ctime, mtime and space used.
*/
static void nfs42_copy_dest_done(struct file *file, loff_t pos, loff_t len,
- loff_t oldsize)
+ loff_t oldsize, const struct nfs_fattr *fattr)
{
struct inode *inode = file_inode(file);
struct address_space *mapping = file->f_mapping;
@@ -405,6 +406,9 @@ static void nfs42_copy_dest_done(struct file *file, loff_t pos, loff_t len,
NFS_INO_INVALID_CTIME |
NFS_INO_INVALID_MTIME |
NFS_INO_INVALID_BLOCKS);
+ /* The server may have stamped c/mtime; stamp after it */
+ if (fattr && nfs_have_delegated_mtime(inode))
+ nfs_delegated_ctime_raise(inode, fattr);
spin_unlock(&inode->i_lock);
nfs_update_delegated_mtime(inode);
}
@@ -506,7 +510,8 @@ static ssize_t _nfs42_proc_copy(struct file *src,
goto out;
}

- nfs42_copy_dest_done(dst, pos_dst, res->write_res.count, oldsize_dst);
+ nfs42_copy_dest_done(dst, pos_dst, res->write_res.count, oldsize_dst,
+ NULL);
nfs_invalidate_atime(src_inode);
status = res->write_res.count;
out:
@@ -1311,7 +1316,8 @@ static int _nfs42_proc_clone(struct rpc_message *msg, struct file *src_f,
/* a zero-length count means clone to EOF in src */
if (count == 0 && res.dst_fattr->valid & NFS_ATTR_FATTR_SIZE)
count = nfs_size_to_loff_t(res.dst_fattr->size) - dst_offset;
- nfs42_copy_dest_done(dst_f, dst_offset, count, oldsize_dst);
+ nfs42_copy_dest_done(dst_f, dst_offset, count, oldsize_dst,
+ res.dst_fattr);
status = nfs_post_op_update_inode(dst_inode, res.dst_fattr);
}


--
2.56.0