[PATCH 24/25] xfs: support non-power-of-two rtextsize with exchange-range

Darrick J. Wong
2023-12-31T20:30:30Z

The VFS exchange-range alignment checks use (fast) bitmasks to perform
block alignment checks on the exchange parameters. Unfortunately,
bitmasks require that the alignment size be a power of two. This isn't
true for realtime devices, so we have to copy-pasta the VFS checks using
long division for this to work properly.

Signed-off-by: Darrick J. Wong <djwong@kernel.org>
---
fs/xfs/xfs_xchgrange.c | 102 +++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 91 insertions(+), 11 deletions(-)

diff --git a/fs/xfs/xfs_xchgrange.c b/fs/xfs/xfs_xchgrange.c
index fd09a2dfca9b9..d805678e946c1 100644
--- a/fs/xfs/xfs_xchgrange.c
+++ b/fs/xfs/xfs_xchgrange.c
@@ -808,6 +808,86 @@ xfs_xchg_range_need_convert_bigalloc(
xfs_inode_has_bigallocunit(ip);
}

+/*
+ * Check the alignment of an exchange request when the allocation unit size
+ * isn't a power of two. The VFS helpers use (fast) bitmask-based alignment
+ * checks, but here we have to use slow long division.
+ */
+static int
+xfs_xchg_range_check_rtalign(
+ struct xfs_inode *ip1,
+ struct xfs_inode *ip2,
+ const struct xfs_exch_range *fxr)
+{
+ struct xfs_mount *mp = ip1->i_mount;
+ uint32_t rextbytes;
+ uint64_t length = fxr->length;
+ uint64_t blen;
+ loff_t size1, size2;
+
+ rextbytes = XFS_FSB_TO_B(mp, mp->m_sb.sb_rextsize);
+ size1 = i_size_read(VFS_I(ip1));
+ size2 = i_size_read(VFS_I(ip2));
+
+ /* The start of both ranges must be aligned to a rt extent. */
+ if (!isaligned_64(fxr->file1_offset, rextbytes) ||
+ !isaligned_64(fxr->file2_offset, rextbytes))
+ return -EINVAL;
+
+ /*
+ * If the caller asked for full files, check that the offset/length
+ * values cover all of both files.
+ */
+ if ((fxr->flags & XFS_EXCH_RANGE_FULL_FILES) &&
+ (fxr->file1_offset != 0 || fxr->file2_offset != 0 ||
+ fxr->length != size1 || fxr->length != size2))
+ return -EDOM;
+
+ if (fxr->flags & XFS_EXCH_RANGE_TO_EOF)
+ length = max_t(int64_t, size1 - fxr->file1_offset,
+ size2 - fxr->file2_offset);
+
+ /*
+ * If the user wanted us to exchange up to the infile's EOF, round up
+ * to the next rt extent boundary for this check. Do the same for the
+ * outfile.
+ *
+ * Otherwise, reject the range length if it's not rt extent aligned.
+ * We already confirmed the starting offsets' rt extent block
+ * alignment.
+ */
+ if (fxr->file1_offset + length == size1)
+ blen = roundup_64(size1, rextbytes) - fxr->file1_offset;
+ else if (fxr->file2_offset + length == size2)
+ blen = roundup_64(size2, rextbytes) - fxr->file2_offset;
+ else if (!isaligned_64(length, rextbytes))
+ return -EINVAL;
+ else
+ blen = length;
+
+ /* Don't allow overlapped exchanges within the same file. */
+ if (ip1 == ip2 &&
+ fxr->file2_offset + blen > fxr->file1_offset &&
+ fxr->file1_offset + blen > fxr->file2_offset)
+ return -EINVAL;
+
+ /*
+ * Ensure that we don't exchange a partial EOF rt extent into the
+ * middle of another file.
+ */
+ if (isaligned_64(length, rextbytes))
+ return 0;
+
+ blen = length;
+ if (fxr->file2_offset + length < size2)
+ blen = rounddown_64(blen, rextbytes);
+
+ if (fxr->file1_offset + blen < size1)
+ blen = rounddown_64(blen, rextbytes);
+
+ return blen == length ? 0 : -EINVAL;
+}
+
/* Prepare two files to have their data exchanged. */
int
xfs_xchg_range_prep(
@@ -818,6 +898,7 @@ xfs_xchg_range_prep(
{
struct xfs_inode *ip1 = XFS_I(file_inode(file1));
struct xfs_inode *ip2 = XFS_I(file_inode(file2));
+ unsigned int alloc_unit = xfs_inode_alloc_unitsize(ip2);
int error;

trace_xfs_xchg_range_prep(ip1, fxr, ip2, 0);
@@ -826,18 +907,17 @@ xfs_xchg_range_prep(
if (XFS_IS_REALTIME_INODE(ip1) != XFS_IS_REALTIME_INODE(ip2))
return -EINVAL;

- /*
- * The alignment checks in the VFS helpers cannot deal with allocation
- * units that are not powers of 2. This can happen with the realtime
- * volume if the extent size is set. Note that alignment checks are
- * skipped if FULL_FILES is set.
- */
- if (!(fxr->flags & XFS_EXCH_RANGE_FULL_FILES) &&
- !is_power_of_2(xfs_inode_alloc_unitsize(ip2)))
- return -EOPNOTSUPP;
+ /* Check non-power of two alignment issues, if necessary. */
+ if (XFS_IS_REALTIME_INODE(ip2) && !is_power_of_2(alloc_unit)) {
+ error = xfs_xchg_range_check_rtalign(ip1, ip2, fxr);
+ if (error)
+ return error;

- error = xfs_exch_range_prep(file1, file2, fxr,
- xfs_inode_alloc_unitsize(ip2));
+ /* Do the VFS checks with the regular block alignment. */
+ alloc_unit = ip1->i_mount->m_sb.sb_blocksize;
+ }
+
+ error = xfs_exch_range_prep(file1, file2, fxr, alloc_unit);
if (error || fxr->length == 0)
return error;