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

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:
KaraZajac
2026-07-23 17:49:40 -04:00
parent d466fbfdcb
commit e46a32f11e
19 changed files with 1914 additions and 31 deletions
+126
View File
@@ -1,3 +1,129 @@
## SKELETONKEY v0.9.14 — new LPE module: refluxfs (CVE-2026-64600)
Adds **`refluxfs` — CVE-2026-64600 "RefluXFS"** (Qualys Threat Research Unit),
taking the corpus to **46 modules / 41 CVEs** and opening a brand-new subsystem:
**XFS reflink copy-on-write** (`fs/xfs/xfs_iomap.c`, `fs/xfs/xfs_reflink.c`). It
is also the corpus's first **data-oriented** kernel bug — every other kernel
entry in the set 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 allocate a transaction
(i.e. to wait for log space). On re-acquiring it, the code re-queries the
refcount btree at the **original** physical block number (`imap->br_startblock`)
and **never re-reads the data fork**. A second `O_DIRECT` writer holding only the
coarser `IOLOCK` can complete an entire CoW cycle inside that window — allocate
block Y, write it, remap via `xfs_reflink_end_cow()` — leaving the first writer's
mapping pointing at a block now owned solely by the reflink **source**. The stale
lookup returns refcount `1`, the writer concludes the block is private, and
writes to it in place, landing attacker data on the source file's on-disk blocks.
Three properties make this unlike anything else in the corpus:
- **No offsets, no ROP, no KASLR/SMEP/SMAP.** The primitive is an arbitrary
overwrite of the *on-disk contents of any readable file*, so there is nothing
to port per kernel build and no `--full-chain` offset entry to fill. Qualys is
explicit that SELinux enforcing, container boundaries and seccomp are equally
irrelevant: *"This isn't a vulnerability you can harden around, isolate, or
live-patch."*
- **File-integrity monitoring cannot see it.** The data is applied to the shared
physical block *beneath* the victim inode. No `write(2)` ever targets it, so
`mtime`/`ctime`/size are unchanged and nothing is logged — `-w /etc/passwd -p
wa`, AIDE and Tripwire all stay silent. The change persists across reboots.
- **The exposure is distro-shaped, not kernel-shaped.** What matters is whether
XFS+reflink is the installer default: **RHEL/CentOS Stream/Rocky/AlmaLinux/
Oracle/CloudLinux 8-10, Fedora Server ≥ 31 and Amazon Linux 2023** are
exploitable out of the box; Debian, Ubuntu, Fedora Workstation, SLES, openSUSE
and Arch default to ext4/btrfs and are not reachable. RHEL/CentOS 7 (3.10) was
never affected.
Introduced **4.11** (2017-02, `3c68d44a2b49`) — a nine-year window. Fixed by
`2f4acd0fcd86` ("xfs: resample the data fork mapping after cycling ILOCK"),
merged **2026-07-16** for **7.2-rc4**; stable backports **7.1.4** / **6.18.39** /
**6.12.96**. The 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 lines have no
upstream stable fix in the CNA record at time of writing. CWE-362 → CWE-367; NVD
published neither a CWE nor a CVSS vector at time of writing; not in CISA KEV.
🟡 **Trigger (reconstructed) — deliberately under-driven, and VM-VERIFIED.**
Unlike the corpus's other race
modules, `detect()` is **not** a pure version gate: this bug's reachability is
safely observable, so it pairs the three-branch version table with a **real
storage precondition** — a writable directory on a mounted XFS filesystem,
identified by `statfs(2)` `f_type == XFS_SUPER_MAGIC` and deliberately **not** by
a successful `FICLONE`, since btrfs implements `FICLONE` too and is unaffected.
No such directory → `PRECOND_FAIL`, the correct verdict on a stock Debian/Ubuntu
host. Under `--active` it confirms `reflink=1` empirically; override with
`SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. On rpm-family hosts it warns explicitly
that RHEL/Oracle/Rocky/Alma backport **without bumping the upstream version** (a
patched el8 kernel still reports `4.18.0-*`), so the verdict reflects the
upstream base version only — check the RHSA/ELSA/ALSA/RLSA erratum.
`exploit()` forks an isolated child that creates a private `mkdtemp` scratch
directory and works **only on two files it owns**: **(A)** it writes a donor,
`FICLONE`-clones it, and confirms the shared extent via **`FIEMAP_EXTENT_SHARED`**
plus an `O_DIRECT` gate — a read-only, deterministic observation that the exact
refcount state the bug misjudges exists here; then **(B)** it races **8**
concurrent `O_DIRECT` 4 KiB writes against the clone with **2**
`ftruncate`/`fdatasync` helpers cycling the `ILOCK`, for at most **16 rounds /
2 s**, and stops — reading the donor back with `O_DIRECT`, because a buffered read
would be served from the page cache the corruption bypasses and would hide a win.
It is deliberately under-driven against the public PoC's 32 writers and 8
helpers, and it **never clones or targets a file it does not own**: the step that
yields root — reflink-cloning `/etc/passwd` and racing writes onto *its* shared
blocks, then `su` — persistently rewrites a system file on disk with no undo, and
is documented but **not bundled**. Always returns `EXPLOIT_FAIL`.
Note the safety inversion versus the other reconstructed triggers: a won race
here corrupts **file data, not kernel memory**, so there is no oops, no KASAN
report and no panic path, and the blast radius is 4 KiB of a scratch file the
module then deletes. `refluxfs` therefore carries safety rank **55** — far above
`bad_epoll` (12) and `ghostlock` (11) — and `--cleanup` sweeps any
`skeletonkey-refluxfs-*` directories left by an interrupted run.
Detection gets a genuinely unusual treatment, because the obvious rule is the one
that fails. auditd/sigma anchor on the two operations the attack cannot avoid —
`ioctl` request **`0x40049409`** (`FICLONE`, matched exactly so it does not flood)
and `openat` with `O_DIRECT` (`& 0x4000`) — plus the post-exploitation euid-0
transition; falco adds the high-fidelity "reflinked a file owned by another user"
condition. And for once the **yara** rule is the right tool for a kernel bug:
since FIM is structurally blind here, it matches the *on-disk artifact* — a
`passwd` file with a password-less root entry or an added uid-0 account. The
module docs also recommend content-hash-vs-`mtime` drift monitoring, which is a
near-zero-false-positive detector for this entire bug class.
14 new `detect()` unit rows cover the backport boundaries, the 4.11 introduction
gate, the el8/el9 upstream bases, the "newer than some entries but not all" case,
and the no-XFS `PRECOND_FAIL` path (**148 tests total, 0 failures**).
**VM-verified 2026-07-23 — the corpus's first rpm-family verification**, taking
the empirical count to **29 of 41 CVEs**. Target: **Rocky Linux 9.8 /
`5.14.0-687.10.1.el9_8.0.1.x86_64`** under qemu/KVM with 6 vCPUs. The stock
GenericCloud layout needed **no provisioner changes at all** — root is
`/dev/vda4` XFS with `reflink=1` out of the box, which is precisely why this CVE
hits the RHEL family so broadly. `detect()` returned `VULNERABLE`, the
rpm-family vendor-backport caveat fired, the `--active` FICLONE witness confirmed
reflink, phase A observed `FIEMAP_EXTENT_SHARED` on a real shared extent, the
scratch dir self-cleaned, and the source built clean on el9 gcc.
The **underlying bug was separately confirmed winnable** on that kernel using a
VM-only harness driven at the public PoC's parameters (32 writers / 8 helpers,
60 s — `tools/verify-vm/refluxfs_verify.c`): **4 out of 4 runs won**, first
divergence after **69, 114, 170 and 494 rounds**. A racing `O_DIRECT` write
landed on a still-shared block and rewrote the donor's on-disk bytes — the
arbitrary-overwrite primitive observed directly, contained to files the test user
owned, with no oops and no dmesg output (as expected for a data-oriented bug).
Worth stating plainly, because it is the whole point of the design: the shipped
trigger **did not win** in its 2 s budget on a kernel that is provably
vulnerable. That is intended under-driving, not a defect — and it is the concrete
reason a non-win must **never** be recorded as "patched". Trust the version gate
and the vendor erratum.
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.
---
## SKELETONKEY v0.9.13 — new LPE module: ghostlock (CVE-2026-43499)
Adds **`ghostlock` — CVE-2026-43499 "GhostLock"** (VEGA / Nebula Security,