Linux XFS filesystem development
 help / color / mirror / Atom feed
From: Norbert Szetei <norbert@doyensec.com>
To: "Darrick J. Wong" <djwong@kernel.org>
Cc: Carlos Maiolino <cem@kernel.org>,
	linux-xfs@vger.kernel.org, Christoph Hellwig <hch@infradead.org>,
	Dave Chinner <dgc@kernel.org>
Subject: Re: [PATCH] xfs: fix exchange-range-to-eof file size exchange
Date: Tue, 6 Oct 2026 22:02:26 +0200	[thread overview]
Message-ID: <471DF337-49EB-46CE-B07F-45D09B899E8B@doyensec.com> (raw)
In-Reply-To: <20261006050914.GU2705364@frogsfrogsfrogs>

On Oct 6, 2026, at 07:09, Darrick J. Wong <djwong@kernel.org> wrote:
> 
> From: Darrick J. Wong <djwong@kernel.org>

Thanks for picking this up so fast, and for porting the reproducer to 
fstests.

> 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.

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

The reflink flag could be cleared too.

Start:
	peer    size  4096                       -> [100]
	file1   size 12288                       -> [200] [201] [202]
	dirty   i_size 4096   i_disk_size 0
	donor   size  4096                       -> [300]

1. FICLONERANGE peer's block into file1's middle block:

	peer    size  4096                       -> [100]
	file1   size 12288   reflink flag set    -> [200] [100] [202]
	                                                  ^^^^^
	                                      both files own block 100

2. EXCHANGE_RANGE file1 against dirty, file1_offset 8192,
   file2_offset 4096, length 0, TO_EOF:

	length = max(12288 - 8192, 4096 - 4096) = 4096
	dirty's flush range [4096, 8191] is past its EOF, nothing written
	back, so i_disk_size is still 0:

	xmi_isize1 = 8192 + (0 - 4096) = 4096

	file1   i_size 8192  i_disk_size 4096    -> [200] [100] hole
	                                ^^^^
	           one block by the size, two are mapped, and the second
	           one is shared with peer

3. EXCHANGE_RANGE file1 against donor, both offsets 0, length 4096.  By
   i_disk_size this is two one-block files exchanging everything, so
   xmi_can_exchange_reflink_flags() agrees:

	file1   i_disk_size 4096  reflink CLEARED -> [300] [100] hole
	donor                                     -> [200]

   Block 100 is still shared with peer.

4. pwrite(file1, 4096, 4096), file1's second block.
   xfs_is_cow_inode(file1) is false, so no CoW:

	peer    size 4096                        -> [100] new contents


PoC, run after applying your fix:

$ head -c 4096 /dev/zero | tr '\0' 'P' > peer
$ sudo chown root:root peer && sudo chmod 0644 peer
$ ./xfs_exchrange_reflink_after_fix

peer uid 0 mode 0644 size 4096, block size 4096

start                        peer [32]   file1 isize 12288  [13] [14] [15]
1 FICLONERANGE               peer [32]   file1 isize 12288  [13] [32] [15]
2 EXCHANGE_RANGE TO_EOF      peer [32]   file1 isize  8192  [13] [32] [0]
3 EXCHANGE_RANGE             peer [32]   file1 isize  8192  [24] [32] [0]
4 pwrite(file1, blksz)       peer [32]   file1 isize  8192  [24] [32] [0]

peer first byte P -> X   PEER REWRITTEN

Content of xfs_exchrange_reflink_after_fix.c:

// SPDX-License-Identifier: GPL-2.0
/*
 * Clears XFS_DIFLAG2_REFLINK from a file that still owns a shared block, on a
 * kernel carrying "xfs: fix exchange-range-to-eof file size exchange".
 *
 * Needs reflink=1 and exchange=1, and a readable file called "peer" in the
 * current directory.  Brackets are physical block numbers from FIEMAP.
 * Exits 0 if peer was rewritten, 1 if it survived, 2 on a setup error.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <linux/fs.h>
#include <linux/fiemap.h>
#include <errno.h>

#ifndef FICLONERANGE
#define FICLONERANGE _IOW(0x94, 13, struct file_clone_range)
#endif

/* fs/xfs/libxfs/xfs_fs.h */
struct xfs_exchange_range {
	__s32		file1_fd;
	__u32		pad;
	__u64		file1_offset;
	__u64		file2_offset;
	__u64		length;
	__u64		flags;
};
#define XFS_IOC_EXCHANGE_RANGE		_IOW('X', 129, struct xfs_exchange_range)
#define XFS_EXCHANGE_RANGE_TO_EOF	(1ULL << 0)


static unsigned long blksz;
static int peer, f1;

static unsigned long long phys(int fd, unsigned long long off)
{
	struct { struct fiemap f; struct fiemap_extent e[1]; } q;

	memset(&q, 0, sizeof q);
	q.f.fm_start = off;
	q.f.fm_length = blksz;
	q.f.fm_extent_count = 1;
	if (ioctl(fd, FS_IOC_FIEMAP, &q.f) || q.f.fm_mapped_extents < 1)
		return 0;
	return (q.e[0].fe_physical + (off - q.e[0].fe_logical)) / blksz;
}

static void show(const char *tag)
{
	struct stat st;
	int i;

	fstat(f1, &st);
	printf("%-28s peer [%llu]   file1 isize %5lld  [", tag, phys(peer, 0),
	       (long long)st.st_size);
	for (i = 0; i < 3; i++)
		printf("%llu%s", phys(f1, (unsigned long long)i * blksz),
		       i == 2 ? "]\n" : "] [");
}

/* fsync == 0 leaves every block dirty, so i_disk_size stays 0 */
static int mkfile(const char *name, char fill, int blocks, int do_fsync)
{
	char *buf;
	int fd;

	unlink(name);
	fd = open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
	if (fd < 0)
		return fprintf(stderr, "open %s: %m\n", name), -1;
	buf = malloc((size_t)blocks * blksz);
	memset(buf, fill, (size_t)blocks * blksz);
	if (pwrite(fd, buf, (size_t)blocks * blksz, 0) !=
	    (ssize_t)((size_t)blocks * blksz))
		return fprintf(stderr, "pwrite %s: %m\n", name), -1;
	free(buf);
	if (do_fsync)
		fsync(fd);
	return fd;
}

static int exchange(int fd2, int fd1, unsigned long long off1,
		    unsigned long long off2, unsigned long long len,
		    unsigned long long flags, const char *what)
{
	struct xfs_exchange_range xr;

	memset(&xr, 0, sizeof xr);
	xr.file1_fd = fd1;
	xr.file1_offset = off1;
	xr.file2_offset = off2;
	xr.length = len;
	xr.flags = flags;
	if (ioctl(fd2, XFS_IOC_EXCHANGE_RANGE, &xr)) {
		fprintf(stderr, "%s: %m\n", what);
		return -1;
	}
	return 0;
}

int main(void)
{
	struct file_clone_range cr;
	char before[65536], after[65536], *buf;
	struct statfs sfs;
	struct stat st;
	int dirty, d2;

	if (statfs(".", &sfs))
		return fprintf(stderr, "statfs: %m\n"), 2;
	if (sfs.f_type != 0x58465342)	/* XFS_SUPER_MAGIC */
		return fprintf(stderr,
			"this directory is not on XFS (statfs type 0x%lx)\n",
			(unsigned long)sfs.f_type), 2;
	blksz = sfs.f_bsize;
	if (blksz < 512 || blksz > 65536)
		return fprintf(stderr, "unexpected block size %lu\n", blksz), 2;

	peer = open("peer", O_RDONLY);
	if (peer < 0)
		return fprintf(stderr, "open peer: %m\n"), 2;
	if (fstat(peer, &st))
		return fprintf(stderr, "fstat peer: %m\n"), 2;
	printf("peer uid %u mode 0%o size %lld, block size %lu\n",
	       st.st_uid, st.st_mode & 07777, (long long)st.st_size, blksz);
	if ((unsigned long)st.st_size < blksz)
		return fprintf(stderr,
			"peer must be at least one block\n"), 2;
	if (pread(peer, before, blksz, 0) != (ssize_t)blksz)
		return fprintf(stderr, "pread peer: %m\n"), 2;
	syncfs(peer);

	/* file1: three blocks, clean, so its own i_disk_size is honest */
	f1 = mkfile("file1", 'A', 3, 1);
	if (f1 < 0)
		return 2;
	/* the lever: one block, entirely dirty, i_disk_size == 0 */
	dirty = mkfile("dirty", 'D', 1, 0);
	if (dirty < 0)
		return 2;
	d2 = mkfile("donor2", 'E', 1, 1);
	if (d2 < 0)
		return 2;
	printf("\n");
	show("start");

	/* share peer's block into file1's middle block */
	memset(&cr, 0, sizeof cr);
	cr.src_fd = peer;
	cr.src_offset = 0;
	cr.src_length = blksz;
	cr.dest_offset = blksz;
	if (ioctl(f1, FICLONERANGE, &cr)) {
		fprintf(stderr, "FICLONERANGE: %m\n");
		if (errno == EOPNOTSUPP)
			fprintf(stderr,
				"this filesystem needs reflink=1, check xfs_info\n");
		return 2;
	}
	show("1 FICLONERANGE");

	/*
	 * ip1 = file1, ip2 = dirty.  off2 == i_size(dirty) == blksz, so
	 *	length = max(3*blksz - 2*blksz, blksz - blksz) = blksz
	 * and dirty's flush range [blksz, 2*blksz) is entirely past its EOF,
	 * so its page cache is never written back and i_disk_size stays 0:
	 *	isize1 = 2*blksz + (0 - blksz) = blksz
	 * file1's ondisk size becomes one block while it still owns the
	 * shared block at offset blksz.
	 */
	if (exchange(dirty, f1, 2 * blksz, blksz, 0,
		     XFS_EXCHANGE_RANGE_TO_EOF, "exchange TO_EOF"))
		return 2;
	show("2 EXCHANGE_RANGE TO_EOF");

	/*
	 * By i_disk_size file1 is now a one-block file, so this looks like a
	 * full-contents exchange of two one-block files and
	 * xmi_can_exchange_reflink_flags() clears the flag.
	 */
	if (exchange(f1, d2, 0, 0, blksz, 0, "exchange flag-clear"))
		return 2;
	show("3 EXCHANGE_RANGE");

	/* in-place write to the still-shared block */
	buf = malloc(blksz);
	memset(buf, 'X', blksz);
	if (pwrite(f1, buf, blksz, blksz) != (ssize_t)blksz)
		return fprintf(stderr, "pwrite file1: %m\n"), 2;
	fsync(f1);
	show("4 pwrite(file1, blksz)");

	posix_fadvise(peer, 0, 0, POSIX_FADV_DONTNEED);
	if (pread(peer, after, blksz, 0) != (ssize_t)blksz)
		return fprintf(stderr, "pread peer: %m\n"), 2;
	printf("\npeer first byte %c -> %c   %s\n", before[0], after[0],
	       memcmp(before, after, blksz) ? "PEER REWRITTEN" : "peer intact");
	return memcmp(before, after, blksz) ? 0 : 1;
}

N.

> Link: https://lore.kernel.org/linux-xfs/1E196589-DEBE-40AC-AFEA-D420DAAB067F@doyensec.com/
> Reported-by: Norbert Szetei <norbert@doyensec.com>
> Cc: <stable@vger.kernel.org> # v6.10
> Fixes: 966ceafc7a4371 ("xfs: create deferred log items for file mapping exchanges")
> Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
> ---
> fs/xfs/libxfs/xfs_exchmaps.c |    8 ++++++--
> fs/xfs/xfs_exchrange.c       |   10 ++++++----
> 2 files changed, 12 insertions(+), 6 deletions(-)
> 
> diff --git a/fs/xfs/libxfs/xfs_exchmaps.c b/fs/xfs/libxfs/xfs_exchmaps.c
> index 6a66b6075e0af4..c9d9464e9d39f5 100644
> --- a/fs/xfs/libxfs/xfs_exchmaps.c
> +++ b/fs/xfs/libxfs/xfs_exchmaps.c
> @@ -987,6 +987,7 @@ xfs_exchmaps_init_intent(
> const struct xfs_exchmaps_req *req)
> {
> struct xfs_exchmaps_intent *xmi;
> + struct xfs_mount *mp = req->ip1->i_mount;
> unsigned int rs = 0;
> 
> xmi = kmem_cache_zalloc(xfs_exchmaps_intent_cache,
> @@ -1006,9 +1007,12 @@ xfs_exchmaps_init_intent(
> }
> 
> 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);
> }
> 
> /* Record the state of each inode's reflink flag before the op. */
> diff --git a/fs/xfs/xfs_exchrange.c b/fs/xfs/xfs_exchrange.c
> index fafb4e3f065c75..a4016116f5f1a9 100644
> --- a/fs/xfs/xfs_exchrange.c
> +++ b/fs/xfs/xfs_exchrange.c
> @@ -296,11 +296,13 @@ xfs_exchrange_mappings(
> * the mappings, and updated the ondisk sizes.
> */
> if (fxr->flags & XFS_EXCHANGE_RANGE_TO_EOF) {
> - loff_t temp;
> + loff_t old_ip1_size = i_size_read(VFS_I(ip1));
> + loff_t old_ip2_size = i_size_read(VFS_I(ip2));
> 
> - temp = i_size_read(VFS_I(ip2));
> - i_size_write(VFS_I(ip2), i_size_read(VFS_I(ip1)));
> - i_size_write(VFS_I(ip1), temp);
> + i_size_write(VFS_I(ip2), fxr->file2_offset +
> + (old_ip1_size - fxr->file1_offset));
> + i_size_write(VFS_I(ip1), fxr->file1_offset +
> + (old_ip2_size - fxr->file2_offset));
> }
> 
> out_unlock:


  parent reply	other threads:[~2026-10-06 20:02 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-06  5:09 [PATCH] xfs: fix exchange-range-to-eof file size exchange Darrick J. Wong
2026-10-06  6:15 ` [PATCH] xfs: add regression test for exchangerange-to-eof reflux Darrick J. Wong
2026-10-06  7:25 ` [PATCH] xfs: fix exchange-range-to-eof file size exchange Dave Chinner
2026-10-06 15:00   ` Darrick J. Wong
2026-10-06 20:12     ` Norbert Szetei
2026-10-06 23:40       ` Darrick J. Wong
2026-10-06 20:02 ` Norbert Szetei [this message]
2026-10-06 21:47   ` Darrick J. Wong
2026-10-06 22:43     ` Darrick J. Wong

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=471DF337-49EB-46CE-B07F-45D09B899E8B@doyensec.com \
    --to=norbert@doyensec.com \
    --cc=cem@kernel.org \
    --cc=dgc@kernel.org \
    --cc=djwong@kernel.org \
    --cc=hch@infradead.org \
    --cc=linux-xfs@vger.kernel.org \
    /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