modules: add refluxfs (CVE-2026-64600, "RefluXFS" XFS reflink CoW ILOCK race)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Adds the corpus's first XFS module and its first data-oriented kernel bug —
every other kernel entry corrupts memory; this one corrupts file contents.
xfs_direct_write_iomap_begin() reads the data-fork extent map under ILOCK,
then xfs_reflink_fill_cow_hole() drops ILOCK to wait for transaction log
space. On reacquiring it, the code re-queries the refcount btree at the
ORIGINAL imap->br_startblock and never re-reads the data fork. A second
O_DIRECT writer holding only IOLOCK completes a whole CoW cycle in that
window, so the first writer's stale mapping sees refcount 1, treats a
still-shared block as private, and writes to it in place — landing its data
on the reflink source file's on-disk blocks.
The primitive is an arbitrary overwrite of the on-disk contents of any
readable file, which has three consequences that drive the design:
- No offsets, no ROP, no KASLR/SMEP/SMAP; SELinux, containers and seccomp
are all irrelevant.
- The victim's inode is never written, so mtime/ctime/size never change
and nothing is logged — FIM and `-w /etc/passwd -p wa` cannot see it.
- The change persists across reboots.
Introduced 4.11 (3c68d44a2b49); fixed 2f4acd0fcd86 (mainline 7.2-rc4,
merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96. Exposure is
distro-shaped: RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server
>= 31 and Amazon Linux 2023 ship XFS+reflink by default.
detect() is not a pure version gate — reachability here is safely
observable, so it pairs the backport table with a real storage precondition
(writable XFS via statfs XFS_SUPER_MAGIC, deliberately not via a successful
FICLONE since btrfs implements that too and is unaffected). --active
confirms reflink via FICLONE; SKELETONKEY_XFS_ASSUME_REFLINK=1/0 overrides.
On rpm-family hosts it warns that vendors backport without bumping the
upstream version, so the verdict speaks only to the upstream base.
exploit() forks a child that works only in a private mkdtemp scratch dir on
two files it owns: it establishes a shared extent (FICLONE, corroborated by
FIEMAP_EXTENT_SHARED) plus an O_DIRECT gate, then races a hard-bounded
8 writers / 2 helpers / 16 rounds / 2s and stops, reading the donor back
with O_DIRECT. Deliberately under-driven, and it never clones or targets a
file it does not own — the /etc/passwd overwrite -> su -> root step is
documented but NOT bundled. Always returns EXPLOIT_FAIL.
Safety rank 55, far above bad_epoll (12) and ghostlock (11): a won race
corrupts 4 KiB of our own scratch file and cannot touch kernel memory, so
there is no oops/KASAN/panic path.
Detection inverts the usual advice. auditd/sigma anchor on ioctl request
0x40049409 (FICLONE, matched exactly) and openat O_DIRECT; falco adds the
cross-uid reflink condition; and the yara rule is genuinely the right tool
here, matching the on-disk artifact because FIM is structurally blind.
VM-VERIFIED 2026-07-23 — the corpus's first rpm-family verification, taking
the empirical count to 29 of 41 CVEs. Rocky Linux 9.8 /
5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, stock GenericCloud layout
with no provisioner changes (root is XFS with reflink=1 out of the box).
detect() -> VULNERABLE, --active FICLONE witness confirmed reflink, phase A
observed FIEMAP_EXTENT_SHARED on a real shared extent, scratch self-cleaned,
clean build on el9 gcc. The underlying bug was separately confirmed winnable
on that kernel via tools/verify-vm/refluxfs_verify.c at the public PoC's
parameters (32 writers / 8 helpers, 60s): 4/4 runs won, first divergence
after 69/114/170/494 rounds. The shipped under-driven trigger did NOT win in
its 2s budget on that same vulnerable kernel — intended behaviour, and
exactly why a non-win must never be read as "patched".
14 new detect() unit rows (148 tests total, 0 failures). Bumps to v0.9.14.
Credit: Qualys Threat Research Unit (blog by Saeed Abbasi; the technical
advisory credits model-assisted kernel analysis performed with Anthropic),
and the upstream XFS maintainers who fixed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
This commit is contained in:
@@ -74,6 +74,7 @@ extern const struct skeletonkey_module cifswitch_module;
|
||||
extern const struct skeletonkey_module nft_catchall_module;
|
||||
extern const struct skeletonkey_module bad_epoll_module;
|
||||
extern const struct skeletonkey_module ghostlock_module;
|
||||
extern const struct skeletonkey_module refluxfs_module;
|
||||
|
||||
static int g_pass = 0;
|
||||
static int g_fail = 0;
|
||||
@@ -1010,6 +1011,127 @@ static void run_all(void)
|
||||
&ghostlock_module, &h_ghl_710,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* ── refluxfs (CVE-2026-64600) ───────────────────────────────
|
||||
* Version gate over a THREE-branch backport table (fixed 7.1.4 /
|
||||
* 6.18.39 / 6.12.96 on-branch, 7.2+ inherits mainline; introduced
|
||||
* 4.11) AND a storage precondition: the XFS reflink CoW race is only
|
||||
* reachable where a writable XFS filesystem is mounted, so a
|
||||
* vulnerable kernel on an ext4/btrfs-only host is PRECOND_FAIL, not
|
||||
* VULNERABLE. We drive that deterministically with
|
||||
* SKELETONKEY_XFS_ASSUME_REFLINK (1=reachable, 0=no XFS) so the rows
|
||||
* don't depend on the CI runner having an XFS volume. Patched and
|
||||
* predates-the-bug rows return OK before the probe is consulted, so
|
||||
* they hold regardless of the override.
|
||||
*
|
||||
* The RHEL-family base versions carry real weight here: 4.18 (el8)
|
||||
* and 5.14 (el9) are the primary affected population and sit below
|
||||
* every table entry. Note those vendors backport without bumping the
|
||||
* upstream version — detect() warns about that at runtime; these rows
|
||||
* pin the upstream-version behaviour only. */
|
||||
setenv("SKELETONKEY_XFS_ASSUME_REFLINK", "1", 1);
|
||||
|
||||
/* 3.10.0 — RHEL/CentOS 7; predates reflink entirely → OK */
|
||||
struct skeletonkey_host h_rfx_3100 =
|
||||
mk_host(h_kernel_6_12, 3, 10, 0, "3.10.0-el7-test");
|
||||
run_one("refluxfs: 3.10.0 (el7) predates XFS reflink → OK",
|
||||
&refluxfs_module, &h_rfx_3100,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 4.10.0 — one below the 4.11 introduction → OK */
|
||||
struct skeletonkey_host h_rfx_4100 =
|
||||
mk_host(h_kernel_6_12, 4, 10, 0, "4.10.0-test");
|
||||
run_one("refluxfs: 4.10.0 one below the 4.11 introduction → OK",
|
||||
&refluxfs_module, &h_rfx_4100,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 4.18.0 — RHEL 8 upstream base, below every entry → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_4180 =
|
||||
mk_host(h_kernel_6_12, 4, 18, 0, "4.18.0-el8-test");
|
||||
run_one("refluxfs: 4.18.0 (el8 base) + XFS → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_4180,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 5.14.0 — RHEL 9 upstream base → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_5140 =
|
||||
mk_host(h_kernel_6_12, 5, 14, 0, "5.14.0-el9-test");
|
||||
run_one("refluxfs: 5.14.0 (el9 base) + XFS → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_5140,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.1.100 — LTS branch with no published backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61100 =
|
||||
mk_host(h_kernel_6_12, 6, 1, 100, "6.1.100-test");
|
||||
run_one("refluxfs: 6.1.100 (LTS, no upstream fix) → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61100,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.12.95 one below the 6.12.96 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61295 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 95, "6.12.95-test");
|
||||
run_one("refluxfs: 6.12.95 below the 6.12.96 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61295,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.12.96 exact backport → OK via patch table */
|
||||
struct skeletonkey_host h_rfx_61296 =
|
||||
mk_host(h_kernel_6_12, 6, 12, 96, "6.12.96-test");
|
||||
run_one("refluxfs: 6.12.96 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_61296,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 6.13.0 — newer than 6.12.96 but OLDER than 6.18.39/7.1.4, an EOL
|
||||
* branch with no fix → must stay VULNERABLE ("newer than ALL" test). */
|
||||
struct skeletonkey_host h_rfx_6130 =
|
||||
mk_host(h_kernel_6_12, 6, 13, 0, "6.13.0-test");
|
||||
run_one("refluxfs: 6.13.0 newer than some entries but not all → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_6130,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.18.38 one below the 6.18.39 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_61838 =
|
||||
mk_host(h_kernel_6_12, 6, 18, 38, "6.18.38-test");
|
||||
run_one("refluxfs: 6.18.38 below the 6.18.39 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_61838,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 6.18.39 exact backport → OK */
|
||||
struct skeletonkey_host h_rfx_61839 =
|
||||
mk_host(h_kernel_6_12, 6, 18, 39, "6.18.39-test");
|
||||
run_one("refluxfs: 6.18.39 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_61839,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 7.1.3 one below the 7.1.4 backport → VULNERABLE */
|
||||
struct skeletonkey_host h_rfx_713 =
|
||||
mk_host(h_kernel_6_12, 7, 1, 3, "7.1.3-test");
|
||||
run_one("refluxfs: 7.1.3 below the 7.1.4 backport → VULNERABLE",
|
||||
&refluxfs_module, &h_rfx_713,
|
||||
SKELETONKEY_VULNERABLE);
|
||||
|
||||
/* 7.1.4 exact backport → OK */
|
||||
struct skeletonkey_host h_rfx_714 =
|
||||
mk_host(h_kernel_6_12, 7, 1, 4, "7.1.4-test");
|
||||
run_one("refluxfs: 7.1.4 (exact backport) → OK via patch table",
|
||||
&refluxfs_module, &h_rfx_714,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* 7.2.0 newer than every entry → mainline-inherited fix
|
||||
* (2f4acd0fcd86 shipped in 7.2-rc4) → OK */
|
||||
struct skeletonkey_host h_rfx_720 =
|
||||
mk_host(h_kernel_6_12, 7, 2, 0, "7.2.0-test");
|
||||
run_one("refluxfs: 7.2.0 above all backports → OK (mainline inherit)",
|
||||
&refluxfs_module, &h_rfx_720,
|
||||
SKELETONKEY_OK);
|
||||
|
||||
/* Same vulnerable kernel, but no writable XFS filesystem mounted —
|
||||
* the stock Debian/Ubuntu (ext4) case. The bug is unreachable, so the
|
||||
* honest verdict is PRECOND_FAIL rather than VULNERABLE. */
|
||||
setenv("SKELETONKEY_XFS_ASSUME_REFLINK", "0", 1);
|
||||
run_one("refluxfs: 6.12.95 but no XFS mounted → PRECOND_FAIL",
|
||||
&refluxfs_module, &h_rfx_61295,
|
||||
SKELETONKEY_PRECOND_FAIL);
|
||||
unsetenv("SKELETONKEY_XFS_ASSUME_REFLINK");
|
||||
|
||||
/* ── coverage report ─────────────────────────────────────────
|
||||
* Iterate the runtime registry (populated by skeletonkey_register_*
|
||||
* calls in main()) and warn for any module that was not touched
|
||||
|
||||
Reference in New Issue
Block a user