Files
SKELETONKEY/modules/refluxfs_cve_2026_64600/NOTICE.md
T
KaraZajac e46a32f11e
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
modules: add refluxfs (CVE-2026-64600, "RefluXFS" XFS reflink CoW ILOCK race)
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
2026-07-23 17:49:40 -04:00

118 lines
7.0 KiB
Markdown

# NOTICE — refluxfs (CVE-2026-64600)
## Vulnerability
**CVE-2026-64600** — "RefluXFS", a **time-of-check/time-of-use race** in the
Linux kernel's XFS **reflink copy-on-write** path
(`fs/xfs/xfs_iomap.c` :: `xfs_direct_write_iomap_begin`
`fs/xfs/xfs_reflink.c` :: `xfs_reflink_allocate_cow` /
`xfs_reflink_fill_cow_hole` / `xfs_find_trim_cow_extent`).
A direct-I/O writer reads the data-fork extent map under `ILOCK`, then drops
`ILOCK` to allocate a transaction (waiting for log space). On re-acquiring the
lock it re-queries the refcount btree at the **original** physical block number
(`imap->br_startblock`) and never re-reads the data fork. A concurrent
`O_DIRECT` writer holding only the coarser `IOLOCK` can complete a full CoW
cycle in that window (allocate block Y, write, remap via
`xfs_reflink_end_cow()`), leaving the first writer's `imap` pointing at a block
now owned solely by the reflink **source**. The stale lookup returns refcount
`1`, the writer treats the block as private, and writes to it in place.
The resulting primitive is **not memory corruption**: it is an arbitrary
overwrite of the **on-disk contents of any file the attacker can read**, on any
reflink-enabled XFS volume they can write to. It needs **no kernel offsets, no
ROP, and no KASLR/SMEP/SMAP bypass**, and it is unaffected by SELinux enforcing,
container boundaries or seccomp. Because the write is applied to the shared
physical block *beneath* the victim inode, the victim's `mtime`/`ctime`/size
never change and no kernel log output is produced — **file-integrity monitoring
does not detect it** — and the change persists across reboots.
Reachable by **any unprivileged local user**: no capability, no user namespace,
no crafted filesystem image. Preconditions are only an XFS filesystem mounted
with `reflink=1` (the `mkfs.xfs` default since xfsprogs 5.1) that the user can
write to, plus read access to the target file. NVD class: **CWE-362** (race)
yielding **CWE-367** (TOCTOU); NVD had published neither a CWE nor a CVSS vector
at time of writing. **Not** in CISA KEV (disclosed 2026-07-22).
## Research credit
- **Discovery and research** by the **Qualys Threat Research Unit (TRU)**,
published 2026-07-22 as "RefluXFS: A Linux Kernel Local Privilege Escalation
to Root in XFS (CVE-2026-64600)"
(<https://blog.qualys.com/vulnerabilities-threat-research/2026/07/22/refluxfs-a-linux-kernel-local-privilege-escalation-to-root-in-xfs-cve-2026-64600>),
authored by **Saeed Abbasi**, with the technical advisory at
<https://cdn2.qualys.com/advisory/2026/07/22/RefluXFS.txt> and the disclosure
posted to oss-security
(<https://www.openwall.com/lists/oss-security/2026/07/22/14>). The advisory
credits model-assisted kernel analysis performed with **Anthropic**.
Qualys demonstrated end-to-end root on **RHEL 10.2** by reflink-cloning
`/etc/passwd` into `/var/tmp` and racing concurrent direct-I/O writes to
rewrite it in place. SKELETONKEY's trigger reconstruction uses only the
published shape of that race — the reflink clone, the concurrent `O_DIRECT`
writers, and the `ftruncate`/`fdatasync` helpers that widen the window — and
reuses no exploitation code; it never targets a file it does not own.
- **Introduced** in **4.11** (2017-02) by commit `3c68d44a2b49` ("xfs: allocate
direct I/O COW blocks in iomap_begin").
- **Fixed upstream** by commit
`2f4acd0fcd862e22eab45690ec2c08c80b6ef2e7` ("xfs: resample the data fork
mapping after cycling ILOCK"), merged **2026-07-16** for **7.2-rc4**; stable
backports **7.1.4** (`e705d81a7193`), **6.18.39** (`206c09b04dc5`) and
**6.12.96** (`44f891bc0889`).
- Authoritative version data: the Linux kernel CNA record
(<https://cveawg.mitre.org/api/cve/CVE-2026-64600>,
`git.kernel.org/stable/c/<hash>`). The 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 /
4.18 LTS lines are affected with no upstream stable fix published at time of
writing; RHEL-family, Oracle UEK and Amazon vendor branches backport the fix
**without bumping the upstream base version**, so the vendor erratum
(RHSA / ELSA / ALSA / RLSA) — not `uname -r` — is authoritative there.
All credit for finding, analysing and exploiting this bug belongs to the Qualys
Threat Research Unit and to the upstream XFS maintainers who fixed it.
SKELETONKEY is the bundling and bookkeeping layer only.
## SKELETONKEY role
🟡 **Trigger (reconstructed) — VM-verified, and deliberately not weaponised.**
Confirmed 2026-07-23 on **Rocky Linux 9.8 / `5.14.0-687.10.1.el9_8.0.1.x86_64`**
(stock GenericCloud layout, root on XFS with `reflink=1`) under qemu/KVM:
`detect()` returns `VULNERABLE`, the `--active` FICLONE witness confirms
reflink, and phase A observes `FIEMAP_EXTENT_SHARED` on a real shared extent.
The underlying bug was separately confirmed winnable on that kernel — **4/4
runs, first divergence after 69-494 rounds** in a 60 s budget — via a VM-only
harness at the public PoC's parameters (`tools/verify-vm/refluxfs_verify.c`).
The shipped trigger, deliberately under-driven, did **not** win in its 2 s
budget on that same vulnerable kernel; that is intended, and is why a non-win
must never be read as "patched". See `MODULE.md` for the full result table.
This is the corpus's first XFS
module and its first **data-oriented** kernel bug — every other kernel entry
corrupts memory; this one corrupts file contents.
`detect()` is a kernel-version gate over the three-branch backport table
(7.1.4 / 6.18.39 / 6.12.96, 7.2+ inherits mainline; introduced 4.11) **plus a
real precondition probe**: a writable directory on a mounted XFS filesystem,
identified by `statfs(2)` `f_type == XFS_SUPER_MAGIC` rather than by a working
`FICLONE`, since btrfs implements `FICLONE` too and is unaffected. Under
`--active` it confirms `reflink=1` empirically. Override with
`SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. On rpm-family hosts it explicitly warns
that the upstream-version verdict cannot see a vendor backport.
`exploit()` forks an isolated child that works only inside a private `mkdtemp`
scratch directory, on two files it owns: it confirms a shared extent via
`FIEMAP_EXTENT_SHARED` (a safe, read-only observation of the refcount state the
bug misjudges), then races a hard-bounded 8 writers / 2 helpers / 16 rounds / 2 s
window and stops, reading the donor back with `O_DIRECT` to report divergence
honestly.
It is **deliberately under-driven** (the public PoC uses 32 writers and 8
helpers) and **never clones or targets a file it does not own**. The escalation
step — reflink-cloning a root-owned file such as `/etc/passwd` and racing writes
onto its shared blocks, then `su` — persistently rewrites a system file on disk
with no undo, and is documented in `MODULE.md` but **not bundled**. It always
returns `EXPLOIT_FAIL` and never claims root it did not get.
Unlike the corpus's other reconstructed race triggers, a won race here cannot
touch kernel memory: there is no oops, no KASAN report and no panic risk, and
the blast radius is 4 KiB of our own scratch file. That is why it ranks **55**
in `--auto` safety rather than at the bottom alongside `bad_epoll` (12) and
`ghostlock` (11).