[PATCH] ocfs2: validate move extents against a stable EOF

From: Cen Zhang

Date: Thu Oct 22 2026 - 05:55:45 EST


ocfs2_ioctl_move_extents() checks and clamps the requested range using
several unlocked i_size_read() calls. It takes the inode and cluster
locks later in ocfs2_move_extents(), without rechecking the range.

A concurrent truncate can move EOF below me_start after the first check,
or shrink it between the range comparison and subtraction. The latter
can underflow me_len. A truncate after the initial clamp can also leave
the range extending beyond EOF when extent movement starts. In addition,
the me_start + me_len comparison can wrap for a large userspace length.

Use a helper that reads EOF once, checks me_start first and clamps me_len
with a subtraction-based bound. Normalize the defragmentation threshold
using the same snapshot. Keep the early ioctl validation, then repeat it
after taking the local inode lock, rw lock and cluster inode lock, once
the inode size is stable and refreshed. This rejects a start beyond the
current EOF and bounds the range before selecting or moving extents.

Fixes: 53069d4e7695 ("Ocfs2/move_extents: move/defrag extents within a certain range.")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@xxxxxxxxx>
---
fs/ocfs2/move_extents.c | 49 +++++++++++++++++++++++++++--------------
1 file changed, 32 insertions(+), 17 deletions(-)

diff --git a/fs/ocfs2/move_extents.c b/fs/ocfs2/move_extents.c
index ad1678ee7cc4..ab08c273ca52 100644
--- a/fs/ocfs2/move_extents.c
+++ b/fs/ocfs2/move_extents.c
@@ -907,6 +907,30 @@ out:
return ret;
}

+static int ocfs2_validate_and_adjust_move_range(struct inode *inode,
+ struct ocfs2_move_extents *range)
+{
+ u64 i_size = i_size_read(inode);
+
+ if (range->me_start > i_size)
+ return -EINVAL;
+
+ if (range->me_len > i_size - range->me_start)
+ range->me_len = i_size - range->me_start;
+
+ /*
+ * The default threshold for defragmentation is 1 MiB, which
+ * matches the maximum OCFS2 cluster size.
+ */
+ if (!range->me_threshold)
+ range->me_threshold = 1024 * 1024;
+
+ if (range->me_threshold > i_size)
+ range->me_threshold = i_size;
+
+ return 0;
+}
+
static int ocfs2_move_extents(struct ocfs2_move_extents_context *context)
{
int status;
@@ -936,6 +960,12 @@ static int ocfs2_move_extents(struct ocfs2_move_extents_context *context)
goto out_rw_unlock;
}

+ status = ocfs2_validate_and_adjust_move_range(inode, context->range);
+ if (status) {
+ mlog_errno(status);
+ goto out_inode_unlock;
+ }
+
/*
* remember ip_xattr_sem also needs to be held if necessary
*/
@@ -1028,27 +1058,12 @@ int ocfs2_ioctl_move_extents(struct file *filp, void __user *argp)
goto out_free;
}

- if (range.me_start > i_size_read(inode)) {
- status = -EINVAL;
+ status = ocfs2_validate_and_adjust_move_range(inode, &range);
+ if (status)
goto out_free;
- }
-
- if (range.me_start + range.me_len > i_size_read(inode))
- range.me_len = i_size_read(inode) - range.me_start;

context->range = &range;

- /*
- * ok, the default threshold for the defragmentation
- * is 1M, since our maximum clustersize was 1M also.
- * any thought?
- */
- if (!range.me_threshold)
- range.me_threshold = 1024 * 1024;
-
- if (range.me_threshold > i_size_read(inode))
- range.me_threshold = i_size_read(inode);
-
if (range.me_flags & ~(OCFS2_MOVE_EXT_FL_AUTO_DEFRAG |
OCFS2_MOVE_EXT_FL_PART_DEFRAG)) {
status = -EINVAL;