All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Darrick J. Wong" <djwong@kernel.org>
To: Norbert Szetei <norbert@doyensec.com>
Cc: Dave Chinner <dgc@kernel.org>, Carlos Maiolino <cem@kernel.org>,
	linux-xfs@vger.kernel.org, Christoph Hellwig <hch@infradead.org>
Subject: Re: [PATCH v2] xfs: stop exchanging reflink flags when exchanging mappings
Date: Wed, 7 Oct 2026 09:55:39 -0700	[thread overview]
Message-ID: <20261007165539.GF2705364@frogsfrogsfrogs> (raw)
In-Reply-To: <1E97806E-3236-4BC4-A3F5-8197FE07F077@doyensec.com>

On Wed, Oct 07, 2026 at 04:46:39PM +0200, Norbert Szetei wrote:
> Here is a reproducer for the XFS_IOC_SWAPEXT case, since there wasn't
> one on the list yet, in case it's useful for fstests.
> 
> 	$ truncate -s 512M img && mkfs.xfs -q -m rmapbt=1,reflink=1 img
> 	$ mkdir -p /tmp/m && sudo mount -o loop img /tmp/m && sudo chmod 1777 /tmp/m
> 	$ sudo xfs_io -f -c "pwrite -q -S 0x50 0 4096" /tmp/m/peer
> 	$ sudo chmod 644 /tmp/m/peer
> 	$ cd /tmp/m && ./xfs_swapext_poc
> 
> On an unpatched kernel it reports peer as rewritten. With v2 applied,
> peer is left intact. It needs rmapbt=1.

Thanks for the third repro, I'll kludge that into a fstests too.

--D

> // SPDX-License-Identifier: GPL-2.0
> #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>
> 
> #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)
> 
> typedef struct xfs_bstime {
> 	long		tv_sec;
> 	__s32		tv_nsec;
> } xfs_bstime_t;
> 
> struct xfs_bstat {
> 	__u64		bs_ino;
> 	__u16		bs_mode;
> 	__u16		bs_nlink;
> 	__u32		bs_uid;
> 	__u32		bs_gid;
> 	__u32		bs_rdev;
> 	__s32		bs_blksize;
> 	__s64		bs_size;
> 	xfs_bstime_t	bs_atime;
> 	xfs_bstime_t	bs_mtime;
> 	xfs_bstime_t	bs_ctime;
> 	__s64		bs_blocks;
> 	__u32		bs_xflags;
> 	__s32		bs_extsize;
> 	__s32		bs_extents;
> 	__u32		bs_gen;
> 	__u16		bs_projid_lo;
> 	__u16		bs_forkoff;
> 	__u16		bs_projid_hi;
> 	__u16		bs_sick;
> 	__u16		bs_checked;
> 	unsigned char	bs_pad[2];
> 	__u32		bs_cowextsize;
> 	__u32		bs_dmevmask;
> 	__u16		bs_dmstate;
> 	__u16		bs_aextents;
> };
> 
> struct xfs_swapext {
> 	__s64			sx_version;
> #define XFS_SX_VERSION		0
> 	__s64			sx_fdtarget;
> 	__s64			sx_fdtmp;
> 	__s64			sx_offset;
> 	__s64			sx_length;
> 	char			sx_pad[16];
> 	struct xfs_bstat	sx_stat;
> };
> #define XFS_IOC_SWAPEXT		_IOWR('X', 109, struct xfs_swapext)
> 
> 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("%-26s peer [%llu]   file1 size %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" : "] [");
> }
> 
> static int mkfile(const char *name, char fill, int blocks)
> {
> 	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(blocks * blksz);
> 	memset(buf, fill, blocks * blksz);
> 	if (pwrite(fd, buf, blocks * blksz, 0) != (ssize_t)(blocks * blksz))
> 		return fprintf(stderr, "pwrite %s: %m\n", name), -1;
> 	free(buf);
> 	fsync(fd);
> 	return fd;
> }
> 
> int main(void)
> {
> 	struct xfs_exchange_range xr;
> 	struct file_clone_range cr;
> 	struct xfs_swapext sx;
> 	char before[65536], after[65536], *buf;
> 	struct statfs sfs;
> 	struct stat st;
> 	int d1, d2;
> 
> 	if (statfs(".", &sfs))
> 		return fprintf(stderr, "statfs: %m\n"), 2;
> 	blksz = sfs.f_bsize;
> 
> 	peer = open("peer", O_RDONLY);
> 	if (peer < 0)
> 		return fprintf(stderr, "open peer: %m\n"), 2;
> 	fstat(peer, &st);
> 	printf("peer uid %u mode %04o size %lld, block size %lu\n\n",
> 	       st.st_uid, st.st_mode & 07777, (long long)st.st_size, blksz);
> 	if (pread(peer, before, blksz, 0) != (ssize_t)blksz)
> 		return fprintf(stderr, "read peer: %m\n"), 2;
> 	syncfs(peer);	/* so FIEMAP reports peer's real block */
> 
> 	f1 = mkfile("file1", 'A', 3);
> 	d1 = mkfile("donor1", 'B', 1);
> 	d2 = mkfile("donor2", 'C', 1);
> 	if (f1 < 0 || d1 < 0 || d2 < 0)
> 		return 2;
> 	show("start");
> 
> 	/* 1. share peer's block into the middle of file1 */
> 	cr = (struct file_clone_range){ .src_fd = peer, .src_offset = 0,
> 					.src_length = blksz,
> 					.dest_offset = blksz };
> 	if (ioctl(f1, FICLONERANGE, &cr))
> 		return fprintf(stderr, "FICLONERANGE: %m\n"), 2;
> 	show("1 FICLONERANGE");
> 
> 	/*
> 	 * 2. exchange file1's last block against the whole of donor1 with
> 	 * TO_EOF.  The sizes are exchanged too, so file1 claims one block
> 	 * while still owning three.  file2_offset is block 2, so this exchange
> 	 * cannot clear a reflink flag.
> 	 */
> 	xr = (struct xfs_exchange_range){ .file1_fd = d1, .file1_offset = 0,
> 					  .file2_offset = 2 * blksz,
> 					  .length = 0,
> 					  .flags = XFS_EXCHANGE_RANGE_TO_EOF };
> 	if (ioctl(f1, XFS_IOC_EXCHANGE_RANGE, &xr))
> 		return fprintf(stderr, "EXCHANGE_RANGE TO_EOF: %m\n"), 2;
> 	show("2 EXCHANGE_RANGE TO_EOF");
> 
> 	/*
> 	 * 3. XFS_IOC_SWAPEXT with file1 as the target.  sx_offset is 0 and
> 	 * sx_length equals both inodes' i_disk_size, so the "Verify all data
> 	 * are being swapped" test passes.  xfs_swap_extent_rmap() then bounds
> 	 * its remap loop with XFS_B_TO_FSB(mp, i_size_read(VFS_I(ip))), which
> 	 * is one block, so file1's two mappings above its size are left in
> 	 * place -- and the reflink flags are swapped anyway, clearing file1's.
> 	 */
> 	fsync(f1);
> 	fsync(d2);
> 	if (fstat(f1, &st))
> 		return fprintf(stderr, "fstat file1: %m\n"), 2;
> 	memset(&sx, 0, sizeof sx);
> 	sx.sx_version		= XFS_SX_VERSION;
> 	sx.sx_fdtarget		= f1;
> 	sx.sx_fdtmp		= d2;
> 	sx.sx_offset		= 0;
> 	sx.sx_length		= st.st_size;
> 	sx.sx_stat.bs_mtime.tv_sec  = st.st_mtim.tv_sec;
> 	sx.sx_stat.bs_mtime.tv_nsec = st.st_mtim.tv_nsec;
> 	sx.sx_stat.bs_ctime.tv_sec  = st.st_ctim.tv_sec;
> 	sx.sx_stat.bs_ctime.tv_nsec = st.st_ctim.tv_nsec;
> 	if (ioctl(f1, XFS_IOC_SWAPEXT, &sx))
> 		return fprintf(stderr, "SWAPEXT: %m\n"), 2;
> 	show("3 SWAPEXT");
> 
> 	/* 4. write to the block file1 still shares with peer */
> 	buf = malloc(blksz);
> 	memset(buf, 'X', blksz);
> 	if (pwrite(f1, buf, blksz, blksz) != (ssize_t)blksz)
> 		return fprintf(stderr, "pwrite: %m\n"), 2;
> 	fsync(f1);
> 	show("4 pwrite(file1, 4096)");
> 
> 	posix_fadvise(peer, 0, 0, POSIX_FADV_DONTNEED);
> 	if (pread(peer, after, blksz, 0) != (ssize_t)blksz)
> 		return fprintf(stderr, "re-read 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;
> }
> 
> 

  reply	other threads:[~2026-10-07 16:55 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-07 14:43 [PATCH v2] xfs: stop exchanging reflink flags when exchanging mappings Norbert Szetei
2026-10-07 14:46 ` Norbert Szetei
2026-10-07 16:55   ` Darrick J. Wong [this message]
2026-10-07 16:54 ` Darrick J. Wong
2026-10-07 20:31 ` Dave Chinner

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=20261007165539.GF2705364@frogsfrogsfrogs \
    --to=djwong@kernel.org \
    --cc=cem@kernel.org \
    --cc=dgc@kernel.org \
    --cc=hch@infradead.org \
    --cc=linux-xfs@vger.kernel.org \
    --cc=norbert@doyensec.com \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.