Files
SKELETONKEY/modules/refluxfs_cve_2026_64600/NOTICE.md
T
KaraZajac 5c18b678a5
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
refluxfs: wire the --full-chain /etc/passwd root pop (🟡 trigger -> 🟢 full chain)
Promotes refluxfs (CVE-2026-64600) to a 🟢 full-chain module. VM-verified
end-to-end on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64 under
qemu/KVM, unprivileged uid=1000, SELinux Enforcing:

  skeletonkey --exploit refluxfs --i-know --full-chain

reflink-clones /etc/passwd, races the CoW window (32 writers / 8 helpers),
strips root's password field on-disk (root -> root::, the public PoC's
technique), evicts the stale page cache, verifies via O_DIRECT and returns
EXPLOIT_OK. `su root` (empty password) then gives uid 0. 3/3 wins on a
private-extent target (1244/3716/7913 rounds, 4-30s).

Safety properties (this bug rewrites the block device permanently, unlike
the page-cache 🟢 modules):
  - Crafts the payload FIRST and refuses unless it can preserve both root
    and the invoking user's line; every other passwd line is kept
    byte-for-byte. A tail-truncating port drops sshd/nobody/the caller and
    bricks login (hit exactly this during development).
  - Backs /etc/passwd up before the race; restores on failure; cleanup()
    restores it (run as root after the pop).
  - Destructive path gated behind --full-chain. Plain --exploit / --auto
    run only the safe own-files trigger (EXPLOIT_FAIL), unchanged.

Exploitability constraint discovered during verification (NOT in the Qualys
writeup): the race only fires when the target's extent is PRIVATE going in.
The block starts at refcount 2 (target + attacker clone), the concurrent CoW
drops it to 1, and the stale writer reads "1 -> private". A file already
reflink-shared with a third file keeps a post-CoW refcount > 1 and is NOT
attackable via that target. Stock Rocky 9 ships /etc/passwd pre-shared and
was unattackable across ~41,000 rounds; rewriting it to a private extent
(byte-identical content, as any useradd/passwd/vipw does) made it fall in
~2,000 rounds. So the exploitable state is the normal administered state.

Also fixed a page-cache staleness bug in the win path: the overwrite
bypasses the target inode, so its clean cached pages are never invalidated
and a buffered read (getpwnam in `su`, or the module's own verify) would see
the OLD passwd. The module now issues POSIX_FADV_DONTNEED on a win and
verifies via O_DIRECT.

detect() --active gains a per-target extent-privacy check: it reports
whether /etc/passwd is private (attackable) or already-shared (not).

88-test unit harness still green (148 total). Docs updated: MODULE.md
(full-chain flow, private-extent precondition, verification tables),
NOTICE.md, CVES.md (🟢 + tier/ops tables), README (15 full-chain / 13
primitive; lands-root list), RELEASE_NOTES, targets.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 18:45:32 -04:00

7.4 KiB
Raw Blame History

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_beginfs/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

🟢 Full chain (--full-chain), 🟡 safe trigger by default — VM-verified end-to-end. 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. --exploit refluxfs --i-know --full-chain reflink-clones /etc/passwd, races the CoW window, strips root's password field on-disk, evicts the stale page cache, and returns EXPLOIT_OK; su root (empty password) then gives uid 0 — verified 3/3 wins on a private-extent target (1244 / 3716 / 7913 rounds, 430 s) as unprivileged uid=1000 under SELinux Enforcing, with every other passwd line preserved and the file backed up + restorable. A key exploitability constraint surfaced in testing (not in the public writeup): the target's extent must be private going in — an already-reflink-shared file keeps a post-CoW refcount > 1 and is not attackable via that target; normal admin churn (useradd/passwd/vipw) produces the exploitable private-extent state. Without --full-chain the module runs a safe own-files reachability trigger only (EXPLOIT_FAIL), deliberately under-driven so a non-win is never read as "patched". See MODULE.md for the full result tables. 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).