From: Andrey Albershteyn <aalbersh@kernel.org>
To: linux-xfs@vger.kernel.org
Cc: cem@kernel.org, cihan.cihan@gmail.com, cmaiolino@redhat.com,
dchinner@redhat.com, dgc@kernel.org, djwong@kernel.org,
floss@jetm.me, hch@lst.de, hemanth.selam@gmail.com,
jiapenglin@tencent.com, johannes.thumshirn@wdc.com,
kees+treewide@kernel.org, norbert@doyensec.com,
syzbot+837bcd54843dd6262f2f@syzkaller.appspotmail.com,
yijiangshan@kylinos.cn, yun.zhou@windriver.com,
zenghongling@kylinos.cn
Subject: [PATCH 28/28] xfs: fix exchange-range-to-eof file size exchange
Date: Thu, 29 Oct 2026 13:06:25 +0100 [thread overview]
Message-ID: <20261029120631.803074-29-aalbersh@kernel.org> (raw)
In-Reply-To: <20261029120631.803074-1-aalbersh@kernel.org>
From: "Darrick J. Wong" <djwong@kernel.org>
Source kernel commit: 7dfb218c829c1acb626e0fe92c5fddb9a26a03e9
Norbert Szetei posted a patch containing an incomplete description of a
bug in XFS_IOC_EXCHANGE_RANGE's TO_EOF flag. The bug tricks
exchange-range into modifying a file so that it has shared blocks
starting beyond EOF, which is never allowed because files never have
written data beyond EOF, and you can only share written data blocks.
This is key to all the incorrect behavior that follows.
Eventually I got from Norbert a description of what's going wrong:
> Fair. Here it is with physical block numbers, 4k blocks. peer is the file
> that gets rewritten and file1 is the one that ends up with the flag
> cleared.
>
> Start, nothing shared:
>
> peer size 4096 -> [100]
> file1 size 12288 -> [200] [201] [202]
> donor1 size 4096 -> [300]
> donor2 size 4096 -> [400]
>
> 1. FICLONERANGE peer's block into file1's middle slot:
>
> peer size 4096 -> [100]
> file1 size 12288 -> [200] [100] [202] reflink flag set
> ^^^^^ both files own block 100
>
> 2. XFS_IOC_EXCHANGE_RANGE file1 against donor1, file1_offset 0,
> file2_offset 8192, length 0, TO_EOF. One block is exchanged, and the sizes
> are exchanged with it:
>
> file1 size 4096 -> [200] [100] [300] reflink flag set
> ^^^^^^^^^^^ still owned, now past the size
> donor1 size 12288 -> [202]
>
> file1 says it is one block long while it still owns three. Nothing was
> unmapped. file2_offset is block 2, so this exchange cannot clear a flag.
>
> 3. XFS_IOC_EXCHANGE_RANGE file1 against donor2, both offsets 0, length 4096.
> By i_disk_size this is two one-block files exchanging everything, so
> xmi_can_exchange_reflink_flags() agrees and the flag is cleared:
>
> file1 size 4096 -> [400] [100] [300] reflink flag CLEARED
> donor2 size 4096 -> [200]
>
> Block 100 is still shared with peer.
>
> 4. pwrite(file1, 4096, 4096), which is file1's second slot, block 100.
> xfs_is_cow_inode(file1) is false, so no CoW:
>
> peer size 4096 -> [100] same block, new contents
>
> Block 100 is the only one that matters here. PoC below, it reads a file
> called peer in the current directory. The filesystem needs reflink and
> exchange-range, which mkfs.xfs 7.x turns on by default.
So yes, TO_EOF is screwing up the file size even if it's actually
exchanging the data block correctly. The following is the intended
behavior of XFS_EXCHANGE_RANGE_TO_EOF, as described by its manpage:
"Ignore the length parameter. All bytes in file1_fd from file1_offset
to EOF are moved to file2_fd, and file2's size is set to
(file2_offset+(file1_length-file1_offset)). Meanwhile, all bytes in
file2 from file2_offset to EOF are moved to file1 and file1's size is
set to (file1_offset+(file2_length-file2_offset))."
In other words, if we only exchange a portion of the file data, then we
should only exchange the portion of the file size that relates to the
exchanged data. Unfortunately, the code swaps the ondisk file size and
the incore file sizes, which is incorrect if the caller passes in a
nonzero offset.
Having confused the file sizes, the PoC uses a second exchange-range
call on what looks like a non-reflinked single-block file. Because the
file size is set incorrectly, exchange-range thinks it's exchanging the
full contents of two files and clears the reflink flag on the broken
file. That enables an extending write of the broken file to rewrite the
shared block that's just past EOF. This has become known colloquially
as refluxfs.
Once the file sizes are set correctly, the second exchange-range no
longer thinks that it's doing a full-contents swap, so it won't clear
the reflink flag on either of its file arguments.
Norbert followed up to an earlier version of this patch with a second
complaint about us not handling the i_disk_size state correctly:
> The offsets are bounded against the incore i_size but subtracted from
> the ondisk i_disk_size, so a file that has not been written back gives
> xmi_isize1 = 0 + (0 - 12288).
>
> For instance, you can do:
>
> $ xfs_io -f -c "pwrite -q 0 4096" -c fsync file1
> $ xfs_io -f -c "pwrite -q 0 12288" \
> -c "exchangerange -s 0 -d 12288 file1" dirty
> $ stat -c %s file1
> 0
> $ stat -c %i file1
> 131
> $ sudo xfs_io -r -c "bulkstat_single 131" . | grep bs_size
> bs_size = 18446744073709539328
That's wrong, and the new file size update logic depends on the ondisk
i_disk_size matching the incore i_size, so flush the pagecache as needed
to push it upwards.
Link: https://lore.kernel.org/linux-xfs/1E196589-DEBE-40AC-AFEA-D420DAAB067F@doyensec.com/
Reported-by: Norbert Szetei <norbert@doyensec.com>
Fixes: 966ceafc7a4371 ("xfs: create deferred log items for file mapping exchanges")
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
Signed-off-by: Carlos Maiolino <cem@kernel.org>
---
libxfs/xfs_exchmaps.c | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/libxfs/xfs_exchmaps.c b/libxfs/xfs_exchmaps.c
index 204d06ed091e..99bf3e956108 100644
--- a/libxfs/xfs_exchmaps.c
+++ b/libxfs/xfs_exchmaps.c
@@ -930,6 +930,7 @@ xfs_exchmaps_init_intent(
const struct xfs_exchmaps_req *req)
{
struct xfs_exchmaps_intent *xmi;
+ struct xfs_mount *mp = req->ip1->i_mount;
xmi = kmem_cache_zalloc(xfs_exchmaps_intent_cache,
GFP_NOFS | __GFP_NOFAIL);
@@ -947,10 +948,18 @@ xfs_exchmaps_init_intent(
return xmi;
}
+ /*
+ * If the caller wanted, set each file's size to that file's exchange
+ * offset + length exchanged from the other file because the ranges in
+ * each file might be different lengths.
+ */
if (req->flags & XFS_EXCHMAPS_SET_SIZES) {
+ loff_t off1 = XFS_FSB_TO_B(mp, xmi->xmi_startoff1);
+ loff_t off2 = XFS_FSB_TO_B(mp, xmi->xmi_startoff2);
+
xmi->xmi_flags |= XFS_EXCHMAPS_SET_SIZES;
- xmi->xmi_isize1 = req->ip2->i_disk_size;
- xmi->xmi_isize2 = req->ip1->i_disk_size;
+ xmi->xmi_isize1 = off1 + (req->ip2->i_disk_size - off2);
+ xmi->xmi_isize2 = off2 + (req->ip1->i_disk_size - off1);
}
if (S_ISDIR(VFS_I(xmi->xmi_ip2)->i_mode) ||
--
2.54.0
next prev parent reply other threads:[~2026-10-29 12:11 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 12:05 [PATCH 00/28] xfsprogs: libxfs sync for v7.3 Andrey Albershteyn
2026-10-29 12:05 ` [PATCH 01/28] xfs: add xfs_metadir_create_file helper Andrey Albershteyn
2026-10-29 12:05 ` [PATCH 02/28] xfs: create quota metadir inodes using xfs_metadir_create_file Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 03/28] xfs: create rtgroup " Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 04/28] xfs: mark internal metadir file creation helpers static Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 05/28] xfs: use kmalloc_objs() instead of kmalloc() in xfs_da_grow_inode_int Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 06/28] xfs: remove spurious XBF_DONE clearing on readahead validation failure Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 07/28] xfs: hide b_flags manipulation from code outside of xfs_buf.c Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 08/28] xfs: validate attr entry pointer before field access Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 09/28] xfs: don't hold buffer locks across sync transaction commit in xfs_sync_sb_buf Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 10/28] treewide: refresh kmalloc_obj() conversions Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 11/28] xfs: fix exchange-range reflink flag clearing issue with INO1_WRITTEN Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 12/28] xfs: initialise error in xfs_defer_finish_one() Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 13/28] xfs: give the deferred barrier op type a name Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 14/28] xfs: report the error that made deferred work shut down the fs Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 15/28] xfs: correct the parent pointer space reservation comment Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 16/28] xfs: initialise args->total for parent pointer updates Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 17/28] xfs: assert the reservation covers each da fork growth Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 18/28] xfs: fix the rtrmap and rtrefcount _maxlevels_ondisk functions Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 19/28] xfs: fix xfs_rtrmapbt_mem_cursor for non-rmap filesystems Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 20/28] xfs: preserve owner on in-memory btree creation Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 21/28] xfs: don't leak new_bp if xfs_btree_bload_drop_buf fails Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 22/28] xfs: fix under-reservation of blocks when repairing sf directories Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 23/28] xfs: remove duplicate INO1_WRITTEN check Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 24/28] xfs: fix typos and repeated words in comments Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 25/28] xfs: don't let hidden_space go negative in xfs_metafile_resv_init Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 26/28] xfs: fix wild memcpy access when formatting ondisk rtrefcount btree roots Andrey Albershteyn
2026-10-29 12:06 ` [PATCH 27/28] xfs: stop exchanging reflink flags when exchanging mappings Andrey Albershteyn
2026-10-29 12:06 ` Andrey Albershteyn [this message]
2026-10-29 12:25 ` [PATCH 00/28] xfsprogs: libxfs sync for v7.3 Christoph Hellwig
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261029120631.803074-29-aalbersh@kernel.org \
--to=aalbersh@kernel.org \
--cc=cem@kernel.org \
--cc=cihan.cihan@gmail.com \
--cc=cmaiolino@redhat.com \
--cc=dchinner@redhat.com \
--cc=dgc@kernel.org \
--cc=djwong@kernel.org \
--cc=floss@jetm.me \
--cc=hch@lst.de \
--cc=hemanth.selam@gmail.com \
--cc=jiapenglin@tencent.com \
--cc=johannes.thumshirn@wdc.com \
--cc=kees+treewide@kernel.org \
--cc=linux-xfs@vger.kernel.org \
--cc=norbert@doyensec.com \
--cc=syzbot+837bcd54843dd6262f2f@syzkaller.appspotmail.com \
--cc=yijiangshan@kylinos.cn \
--cc=yun.zhou@windriver.com \
--cc=zenghongling@kylinos.cn \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox